From dfa4b3e466b73213102b578702df876daaa8c325 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 08:40:01 -0400 Subject: [PATCH 001/613] Initial commit. --- dataprojects/__init__.py | 0 .../__pycache__/__init__.cpython-34.pyc | Bin 0 -> 153 bytes dataprojects/__pycache__/admin.cpython-34.pyc | Bin 0 -> 198 bytes .../__pycache__/models.cpython-34.pyc | Bin 0 -> 700 bytes dataprojects/__pycache__/urls.cpython-34.pyc | Bin 0 -> 294 bytes dataprojects/__pycache__/views.cpython-34.pyc | Bin 0 -> 1408 bytes dataprojects/admin.py | 3 + dataprojects/apps.py | 5 + dataprojects/fixtures/dataprojects.json | 32 +++++ dataprojects/migrations/0001_initial.py | 26 ++++ dataprojects/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-34.pyc | Bin 0 -> 1018 bytes .../__pycache__/__init__.cpython-34.pyc | Bin 0 -> 164 bytes dataprojects/models.py | 8 ++ dataprojects/tests.py | 3 + dataprojects/urls.py | 6 + dataprojects/views.py | 40 ++++++ db.sqlite3 | Bin 0 -> 56320 bytes hypatio/__init__.py | 0 hypatio/__pycache__/__init__.cpython-34.pyc | Bin 0 -> 148 bytes hypatio/__pycache__/settings.cpython-34.pyc | Bin 0 -> 3013 bytes hypatio/__pycache__/urls.cpython-34.pyc | Bin 0 -> 1082 bytes hypatio/__pycache__/wsgi.cpython-34.pyc | Bin 0 -> 570 bytes hypatio/settings.py | 135 ++++++++++++++++++ hypatio/urls.py | 23 +++ hypatio/wsgi.py | 16 +++ manage.py | 22 +++ templates/dataprojects/blurb.html | 19 +++ templates/dataprojects/list.html | 106 ++++++++++++++ templates/tokenlogin/token_redirect.html | 10 ++ tokenlogin/__init__.py | 0 .../__pycache__/__init__.cpython-34.pyc | Bin 0 -> 151 bytes .../auth0authenticate.cpython-34.pyc | Bin 0 -> 1158 bytes tokenlogin/__pycache__/urls.cpython-34.pyc | Bin 0 -> 500 bytes tokenlogin/__pycache__/views.cpython-34.pyc | Bin 0 -> 2447 bytes tokenlogin/admin.py | 3 + tokenlogin/apps.py | 5 + tokenlogin/auth0authenticate.py | 28 ++++ tokenlogin/migrations/__init__.py | 0 tokenlogin/models.py | 3 + tokenlogin/tests.py | 3 + tokenlogin/urls.py | 11 ++ tokenlogin/views.py | 58 ++++++++ 43 files changed, 565 insertions(+) create mode 100644 dataprojects/__init__.py create mode 100644 dataprojects/__pycache__/__init__.cpython-34.pyc create mode 100644 dataprojects/__pycache__/admin.cpython-34.pyc create mode 100644 dataprojects/__pycache__/models.cpython-34.pyc create mode 100644 dataprojects/__pycache__/urls.cpython-34.pyc create mode 100644 dataprojects/__pycache__/views.cpython-34.pyc create mode 100644 dataprojects/admin.py create mode 100644 dataprojects/apps.py create mode 100644 dataprojects/fixtures/dataprojects.json create mode 100644 dataprojects/migrations/0001_initial.py create mode 100644 dataprojects/migrations/__init__.py create mode 100644 dataprojects/migrations/__pycache__/0001_initial.cpython-34.pyc create mode 100644 dataprojects/migrations/__pycache__/__init__.cpython-34.pyc create mode 100644 dataprojects/models.py create mode 100644 dataprojects/tests.py create mode 100644 dataprojects/urls.py create mode 100644 dataprojects/views.py create mode 100644 db.sqlite3 create mode 100644 hypatio/__init__.py create mode 100644 hypatio/__pycache__/__init__.cpython-34.pyc create mode 100644 hypatio/__pycache__/settings.cpython-34.pyc create mode 100644 hypatio/__pycache__/urls.cpython-34.pyc create mode 100644 hypatio/__pycache__/wsgi.cpython-34.pyc create mode 100644 hypatio/settings.py create mode 100644 hypatio/urls.py create mode 100644 hypatio/wsgi.py create mode 100755 manage.py create mode 100644 templates/dataprojects/blurb.html create mode 100644 templates/dataprojects/list.html create mode 100644 templates/tokenlogin/token_redirect.html create mode 100644 tokenlogin/__init__.py create mode 100644 tokenlogin/__pycache__/__init__.cpython-34.pyc create mode 100644 tokenlogin/__pycache__/auth0authenticate.cpython-34.pyc create mode 100644 tokenlogin/__pycache__/urls.cpython-34.pyc create mode 100644 tokenlogin/__pycache__/views.cpython-34.pyc create mode 100644 tokenlogin/admin.py create mode 100644 tokenlogin/apps.py create mode 100644 tokenlogin/auth0authenticate.py create mode 100644 tokenlogin/migrations/__init__.py create mode 100644 tokenlogin/models.py create mode 100644 tokenlogin/tests.py create mode 100644 tokenlogin/urls.py create mode 100644 tokenlogin/views.py diff --git a/dataprojects/__init__.py b/dataprojects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dataprojects/__pycache__/__init__.cpython-34.pyc b/dataprojects/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0548250709dc17e5c42702a1235c2d0eebaaedf1 GIT binary patch literal 153 zcmaFI!^>rqdO4f{2p)q77+?f49Dul(1xTbYFa&Ed`mJOr0tq9CUvB!L#i>Qb`nkEu zDWz#?nW_4EWtl}KrHMJId1b}=8I=WzC7JnfIwi3rv7jhFD>b>KSU)~KGcU6wK3=b& V@)n0pZhlH>PO2Tq{9+(x002SFD0lz> literal 0 HcmV?d00001 diff --git a/dataprojects/__pycache__/admin.cpython-34.pyc b/dataprojects/__pycache__/admin.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e191ac99855259761737dbc94827cabca3e2ec62 GIT binary patch literal 198 zcmaFI!^>rqdO6&lfq~&M5W@izkmUfx#Uel=1&A0Kau^tL85yD&85mQT7=ksKUjk(q zG#PKPCZ^m}#sl@w(r6@d&{$xy@$q`<^4XZ_IP)S_bj+}z}p z(zLY9RDHd&%%YOg#GKT;vSR&=%7Vm_%zQYVl30>hP?VpQnp{$>54J_Gpz;=nO>TZl PX-=vgBhcJpAZ7pn%IVw z!of#4ap3{Duww_d;j&IK+{jLx=3C3*3XH zZ`w56$ixD}$2!2a;12vW_%9~3m`)258FgQHx)!t5!q_>!U}B32fh9P~wvNZNqQ7Lh zKgpErd?iTuw{HNkMadyE8%GFX#PLQ=P`gg{rcu zuClVo?sstNc+AyIW-F7q?VT)2S@m0~$sp1pv229sE+KfHwcr>hgk05Bm_CF^*GP#D z7E~T)TrgCF`83Wp?R3mGopeu`PNbFk19BHhta}@e&J>qwl;<;6&*vE*4cD2hupckh zYP7gnQ55{|$?$+aM#LKX#fPgK=^>vn6{1U=Z}zg;$-vhUThij99I|PnKKN-0?dy$_ VChB3m4u3SW@8N$6$_%{g{slLRu(ALE literal 0 HcmV?d00001 diff --git a/dataprojects/__pycache__/urls.cpython-34.pyc b/dataprojects/__pycache__/urls.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de7b3be736f3c532fa5188fe0d42598ab3fbeee9 GIT binary patch literal 294 zcmYjLyGq4C5Ivhs^x^~Z5jJVEot21)wT+07LI~N7?qxR{b~ll0W8v5M5&nmywYrTT zVCAgB#bM6O;V_3YUq{CW&HKmQ3xH3;t~jv^LO7)%sTmAl85#yAGe{1T!E!VlOfkqD zisLmyPbWEKftzY?OI}oL;gdI8_Qy-3L%eEZJ9@WPT@26r_el~b*Hy8Ua@`IOuI}8> z*W?O5BimBUT0I4yCw@rs*v6>*5Z>Tdr2UTXOK}^N55n3`PJOSHsAuit)EYI+K|F5A erQL4Je?N#6Rgarxb!MHMj5<%>hLl&blK%k3(oR_b literal 0 HcmV?d00001 diff --git a/dataprojects/__pycache__/views.cpython-34.pyc b/dataprojects/__pycache__/views.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e32e4f147b47dffe4eb49670a8c1b5edc6371a2f GIT binary patch literal 1408 zcmY*Z&5qkP5FSdF+P-GLxExswSyw{DG1VdUBqp=DY_1l@*>D4*Oo#_4oPpY z3p^(K4 zAL3BI;l%JGAOJD~lmM~31HysK1?pD33!(;@2h^+h8VDb<0BBJ09*8>7dT9&k2GB-H z`yiT-wScxOy#Pc9vM$i>`*R`C5O@e_5BLGxVhneHyTH&G+GxU0q!bD3(cnbIee6U8$;} zNR%F|bS4&#HIGk#ql;sVLmiV6rJVy?fD}Ir3B_;W`b{ZuAjN=*&G^=VoeR1KsRvsm zd`JV(bx1K6$L=!JtickMy;9ypp|h*O7C*Gy0_MYg-G;PdMLz>ruMWsxKzFSM(rAU` z0MB2&g_`vg!pkPeKY$RTdypOg3oK;hM^t!$iXR01r$u(>K^|Isk*Idji@pQtA*4q( zx1A5VU%DfZxUltYB}Q8hSPK`-q79>47wgeEr}eW(-%X=$p3iQa&m-E5#j=ozUS*1U zk1xLXazyL#h9x{-a2g=umxml0NL$yt=- zre^umW7))tm|mhHTTq_sWD#r5OfY`>^xNs=!njMWjhnvK#%Fv{FmC*LtoZ1jY0O9W zDv`ts0jB-cv)SXj(Zyu^!(?W{iW@(fPR?i1^vnc`E0q+vX|XiUmqqE&wD;Xg+a~m2 znqS2tVPzr{DGO=*GHz)+ZdqyXn+b}!ZA@j{SO{qw?KCifTMJAj%Hxce zS<)tE5h*RPhfT1KuSF3v*(hD2iO9kg{oljCg8UeF{v8jWAx>(TWec{syiE9TaFs~C zi3QKE)NpmZj&)MJU-uy&UM2js8mzBP&t8bLy&Gb$g8CFEaC|o;F6oocNSky>NPKcc zdZdrihU4Q19eIL!r$@9`;WkjKLh8jvt1@ZY$Bc@6xhe!3*mB5N7g9+V57EQ@cm^3( zPpAsLcSRU1iu|%HpR_T_9!{qF&Jpe*Saa#yoxa^U{BUPri&qxS1hzsJ*ar5?6We9d v)T0voD3Tvp3nzB6pzckdAZzOPF|v)|5AeT$e+Qg;2!Vau_?D|1IxhJS@A detailed database of health information for the US Population.
", + "short_description": "An instance of i2b2/tranSMART loaded with the publicly available NHANES dataset." + } + }, + { + "model": "dataprojects.dataproject", + "pk": 3, + "fields": { + "name": "GRDR", + "institution": "HMS", + "description": "
An accumulation of data on rare disease.

", + "short_description": "An instance of i2b2/tranSMART loaded with data from the Global Rare Disease Registry Program." + } + } +] \ No newline at end of file diff --git a/dataprojects/migrations/0001_initial.py b/dataprojects/migrations/0001_initial.py new file mode 100644 index 00000000..5a2ce698 --- /dev/null +++ b/dataprojects/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-10-17 14:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DataProject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Name of project')), + ('institution', models.CharField(blank=True, max_length=255, null=True, verbose_name='Institution')), + ('description', models.CharField(blank=True, max_length=4000, null=True, verbose_name='Description')), + ('short_description', models.CharField(blank=True, max_length=255, null=True, verbose_name='Short Description')), + ], + ), + ] diff --git a/dataprojects/migrations/__init__.py b/dataprojects/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dataprojects/migrations/__pycache__/0001_initial.cpython-34.pyc b/dataprojects/migrations/__pycache__/0001_initial.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec3a127d0aefa7c4ce1bc4c066abebc6ce555afd GIT binary patch literal 1018 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 Kg^O?;$Nqm7>I4M< literal 0 HcmV?d00001 diff --git a/hypatio/__init__.py b/hypatio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hypatio/__pycache__/__init__.cpython-34.pyc b/hypatio/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16a13969e6260679e40757bed6524b38274a9359 GIT binary patch literal 148 zcmaFI!^^cN`Eob|5IhDEFu(|8H~?`m3y?@*UcHR zQ%cj)GE?>S$})>eN)vNZ^U8|#Gb#%bOEU8@==k`|yv&mLc)fzkTO2mI`6;D2sdgZv Ii-DK{0JLT%^#A|> literal 0 HcmV?d00001 diff --git a/hypatio/__pycache__/settings.cpython-34.pyc b/hypatio/__pycache__/settings.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e85fd9666d04b7f40fb3557a5450e66cc38b3a82 GIT binary patch literal 3013 zcmb6bM|0ao_pyl0l8fB!IAN+SB?2Wok?l+xOu{lJEE<8d6I?J5cS)^`LSq3l!Zp7p zzaW3(JMZ;7<&w$Sr@TdxXiAwl1o619w|%?(b9trq*VWQbzn=&AKg@lqxc&iO{=Xam z0Wc8efaJl>fq@JB7QoJfQvjm?P7#bEf$Soj`!MF`%8;tjWJOtxTTwBZzD~t&u#2g=i z@u6&?zV@}%z_!*}hn73>>YN4vb0@q$_Wb(6(bNiRSON_5Gvz zT@veTSj1s&od=d5q>^{*AxA6q)aaJ7wW(~O`cpLOcs{K&7wtRJxLe0>aKO+bv*{NY z0IeMa!IXd9Xb^A2mBdLF!pL(P)NM$Mjli3-5pQH6Hxm270j-Zi+pgP;2X$|Jl9J^T z^hB9oe{I_Hy=^j@Ai#_*po9<;}*k$Uj=Y|N6_d=<)U-x{~Cmj665+*}f7NG`hS<4TFQ| z_5x9=P&WvUrj$q57nq!Kjuk%5aFAh5P~+C3Mu0sT$xd*i#slATCkLKQjMCw&korf) zGO+?H?FElEQf-G3V$+%BQ-#w}=(FHR(bGlum=}Gy=qwFSnbX%3+ML$fX(zY|9M4sn z@z1}Aa(}Ee5jNKFv)W)G#_R$-4Nj8&8E1v&B%I2c{L~Z`4^;)8usc=zjwfA&UQFnJuxr**B8jW}G zFM)}>YJ_=vKANZd;Urpr2T31upgf2^Pi2oMNVf9~%d*YUg9YK^7M!7w@xIP~&YnwK$)7J~8naV>po5b7jb>uLkn;2m`7!huM}K!{>>F=SOGm+p{cnnaiBe z=}>nP9GvKX|E`fFQ@NYQcw*p(cQw!p9qO~uDGOp*9S)vtnyr3UZTG~51Z#HMT5n*s zTNzu|ntR$ntfp+W(~-N<9PV|*rG%yrn@vsEW75Th)amc^hXYA@vl)1}$!$3yiY)i0 z1nE&g6;)CsI5`~NWGB>Ho9}p5x*nnSlVozDCF*X3Toi_bW>jh5m5C=zV=eDr#_Pnt zf?(|iJsIwZ6|6}Ah1N2k_4R=+&bNE|07*!|8P&yVx7})Wv={20W;Q#j zu4}qj-s|_{;xzlcr=mL0y1N~9fQ*YT^qscU>$IDy90gHrsRLEI*ToH4T5}goyy)+> z%;##Sjm$ot24(8LW_Y*wD>5nky9xdU2`)--NrF`b;<_||%}s5a>FJ`2m5$om8LB%N zVZWteK<%z(8vULoDnm@Xz4fSsC?siB(&*w=g=Vk1wkploWE9BFP93Xe5q8( xz5P^+Z{vkrwNNS)a^>7jd<*%6d!>|UmUDNi$U|!Ot`t{t$4|brgg%m|^f%wyw+R3M literal 0 HcmV?d00001 diff --git a/hypatio/__pycache__/urls.cpython-34.pyc b/hypatio/__pycache__/urls.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..094e76e2c5bb4b36ab8dcff7bf59f48122fd128a GIT binary patch literal 1082 zcma)5O>fjN5S=9Z(T38N9ynVFX&b4;l;esbP(?*0!~v*~_ORPD_U>jI+mY=Qy4U_1 z{!*?zap4DWVw_FXRS{An#j#&zp5MH&e{VewpOM>dWdLvsYflg7Z}Bxb4h&#~qX*1~ z+ymjm$OjR?C;-o)0puMJT^M!2<36ws@GdNRu;{~$4-V-94}kUH#%qy&jchc?49PZt zZNir*OqM>t{JKbNrpVdnkK|a%X*Mgh17sL}opW+t>Kp?s*V3GmJTsPPRa$Obf+1Fs ztBij)F*#A12&Fm6J`4#yb88DTq?D=D#B7nsnJTne@YKes5|m4N z7$3f*Rux%lsEek!zoFs#ABjLRGYn5knL2r^eQVakn7m_*B$8yJP}&X&nQA47xSk|M z(SU35ubTL}W~^YXL`=oiz8;L94-T5r=1TAbA``)n25vrx!s9$K=EVip#@2ZMr$gB} z*G@`Yd({g1fLyiy2kUVeabhNpW8=~Sk9$uc=ZBkCPpc`0_Z_2{%VhoBhjk&ET~ z5t%l24p)|~dV=?iTg$Ptj)8pG@%3O(xau z{Sq}^0?>FZVYGes&+6<#cMz;PCbPWiI5(Tz3?i$ z18>0v`O1k455S4pq)3sl?D1&E-_Q8>@x%06@%{Qs1n?8~Rzmq3I_{K$1ej5bK*pdW zm{CGI0Ubk~z$}3XBUm25@(|4j{A^pENSzm|(S)D~-=Iu2HkKXw|l5M2(Wo=b~w} zDmqd#pA+9ZQmJi=Oc@faw+&_ukt@earSU0qi0Q)n#=YQN+QMaWDNJSmc`9tp(eRV( zF7y0!3@^U;GP4kZz zXICH2ZwL3|XAZqrrgFo6=;t|k$ny|e7X~{gSwRD29c>$vQ0SvstJa#35>R3(R@#RL zdx~G7P3hq_{CUN%9JY?vbs<+}sgP%c_A8-@#_@X(|J|Kj)@;Jj%iV{+J|affg + +
+
+ {% autoescape off %}{{ dataproject.description }}{% endautoescape %} +
+
+ + diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html new file mode 100644 index 00000000..e693ed84 --- /dev/null +++ b/templates/dataprojects/list.html @@ -0,0 +1,106 @@ + + + + + + + + + + +
+ + +
+ {% for dataproject in dataprojects %} + {% include "dataprojects/blurb.html" with project_counter=forloop.counter0 %} +
+ {% endfor %} +
+ +
+ + + + \ No newline at end of file diff --git a/templates/tokenlogin/token_redirect.html b/templates/tokenlogin/token_redirect.html new file mode 100644 index 00000000..c483f080 --- /dev/null +++ b/templates/tokenlogin/token_redirect.html @@ -0,0 +1,10 @@ + + + + + TOKEN REDIRECT + + + + + \ No newline at end of file diff --git a/tokenlogin/__init__.py b/tokenlogin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tokenlogin/__pycache__/__init__.cpython-34.pyc b/tokenlogin/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08fba12ede5020aa759f13c182bedb48343ac246 GIT binary patch literal 151 zcmaFI!^`zU{&6@15IhDEFu(|8H~?`m3y?@*UcHR zQ%cj)GE?>S$})>eN)vNZ^U8|#Gb#%bOEUA}bV+`8YFHV%6n|$spSW%KC{+=aA_jydNDvDPLam@M09gtugu!y`OX}1_C-n%3a!6ol^AMft>KK;YrXg>M!^Z2F<@D0|6hwFV5b%>DQpFswY zGw3s5w09tLpzi>8VC(|-V2-*9JWX6kJotz=Jm6KByU?%E?W3Ub9jPE-#(Sl>?8UTO z4F?zdDC!mh2eN`jkRI*gW2EaN?~1zbK~h1+s=0hn>XW+!T%No>Fjb${QW zqf6pBIQSZUJa-_ht`;@gj_WW7U~dG2*SN(Xdb1k`TJwCOQ!#4mqP-RlZ&R+?ciN`H z^C-uqZ`xyB$l8@rzOCWzaCytQd{BG?j!8%8{h}np!a!^H?k6 zj<`1dn`%H1O+1mwZy)JdcxdI81rBbCyb7Mj01ps(Uh>P=Wr;mJIz&MK_`{5-iKeJCmvL z*AMDc#48BEwjG!4u@>8K_E-adTWn@?_D>&l)}P0e0U9)4KzW3s?juZhmVGg{-G&v! z+8-T@U93Fzi)$-?k1CHHd1UKGi@@m_XL1^NHn)=8lMR9}>%nYY%$Y7kDl8O ivmr>LGu1_ZS++Hs)`dmb4;KDDq%Ty_n-=rgmirqPZW6Ho literal 0 HcmV?d00001 diff --git a/tokenlogin/__pycache__/urls.cpython-34.pyc b/tokenlogin/__pycache__/urls.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c05ce4e3d95243160914d05c38afdc9f05ed2a6e GIT binary patch literal 500 zcmYjN%T59@6g_30z@U5t3)wI~U<@B%+!$jriy;$w!O>}lv;zhfZu|;A!C&gmxME}C z)|D*?(#N^y=019Qzmi_(Uk;6+1^paOGVShw6(7PLY-a4gx8WxQWcVyaz0rUbK0EcCoR&! zCdeCQO_Cprt=G%{>#cj9pSDN`H^a8+^T$HY>(R89vyoM(Rm(~=!K$QZt$fQ>;2?D1 z=Bi=~rfX9dMbnUy`W-590S7- cnYzbEi?xX_Ma`ELUAb9l*R;|7C}O(o7G=ecYjq~Rs3guCHLLm|K0m3 z1MnltKg~NC%{aAp9C{GM-$+sz)ypj20sI4W{xJo z%YwfI=F)te0xt(O~QbY_~HFgd!ePoSw=W_gCWy#!=yFkBYda4 zaOeiMBF@1lg>41fJ~pNzdIK=VzXI<7XiW}P7$?MC@Fa{=@J@xdD5v2Dbrdkff{fHq zg^$Fv3bQA`voKzQ8x?pC#>;Xsx+5VJoTRXJh)zsPd<8-EZ3241Cy*5DR^d$!==Y!~ z3SR>!BM&+c6gay{NJ{9pEIj8MFfIT>e*<`%gqtMLfBiP&2Vx3!kbv?teX&2%L<4j1 z!DC@iX<3hx0r`fT*~W{O)<5cXB6YKG<}7a*QkRc>W~L5}`p&MIwWf|s0-Hn+UWJUg z1CJC3mQ&>68xoYw%!Gy)u1BH`!#F7(pB!j<@zvfzv$$D|Q^dDiuk&soEQ>LF-;)e*z0xBez$#0lLF7|pPanZnns)$U-LNa z5Ib~8oE}(=>~69#0qOl^UDT1v#J30lnKWUJZPlddc&^h4ZQ z+C1+*!C|~C?U|GRGXgA)9jG~^N&ke>9Z5kjflZR!MB$Dk<|ER=e8lv6j>#pYB<V zsAHyK#2clw9&L`WYr^1$~}kP<5IB(=$046qJ$R7?5Il4_S_Tq z=;iC$se8PCG#VV(Vytx|t){z2&&sGYvA0?R44uRFwH<|5$Msru(9_>IJ=3>(-Rb$+ z>#Eh*o1WL7@#C4sv_P+&yV|biJ6D6c!%U-gHP<$JOb-R+FR*WQZ*qkGi0yG=pd zBZp|nSnPee)#})@*7%%hzBh9AwcS?dbW3lXZ?`(yOqXX&uEiOo)2!R|Z+1yAY1(7& z(x{KVHV$>G=hu#VXFBcn+AodzGrjFs-HX%mPF=5&=;hYaZnRUo=snqO)Hj36XsC5A z{fHaxWjJx8z%b8ePvez6%zG>>k%;oZT<$cD&S7(}(QF&h$NzUXi_|KQxbKxzDx!so zus~Uam$|Xf=+oN|#IGZ6b$-hWz0YwK%VGzKypj{S@fVcu!tNKjk;9q0uro?3CP5<% zND)&HzbeC`8+{EDT@k8Egn)v1)q3*Uh2vWuZ7!F&ZL9q_4c+5qWk$o z8Y*vKQ60;67+lN?c5=+HYOdaHg*aBg0Tq6xABaO9{Bou&Dypocv> Date: Tue, 18 Oct 2016 09:44:44 -0400 Subject: [PATCH 002/613] Adding requirements file. --- requirements.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..8f83cad8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Django==1.10.1 +django-jquery==3.1.0 +django-stronghold==0.2.8 +jwt==0.3.2 +pycrypto==2.6.1 +PyJWT==1.4.2 +PyMySQL==0.7.9 +requests==2.11.1 From edf814221e73966e4e676a78b1cf2451b4879f1e Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 10:50:59 -0400 Subject: [PATCH 003/613] Updating settings to have static file configurations. --- hypatio/settings.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/hypatio/settings.py b/hypatio/settings.py index 76933c51..df8ca4d5 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -132,4 +132,20 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ +########## STATIC FILE CONFIGURATION +# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = normpath(join(SITE_ROOT, 'hypatio-app', 'assets')) + +# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url STATIC_URL = '/static/' + +# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS +STATICFILES_DIRS = ( + normpath(join(SITE_ROOT, 'static')), +) + +# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) \ No newline at end of file From fbdbeb1ba5737f19b2b67effdf5d79de5b73ed11 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 10:55:01 -0400 Subject: [PATCH 004/613] Copy/Paste error. --- hypatio/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hypatio/settings.py b/hypatio/settings.py index df8ca4d5..8059b5bf 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -128,6 +128,11 @@ USE_TZ = True +# Absolute filesystem path to the Django project directory: +DJANGO_ROOT = dirname(dirname(abspath(__file__))) + +# Absolute filesystem path to the top-level project folder: +SITE_ROOT = dirname(DJANGO_ROOT) # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ From 1b1ebe20318aa7b657b2c13034592e1f825d58b4 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 10:56:53 -0400 Subject: [PATCH 005/613] More errors in settings change. --- hypatio/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypatio/settings.py b/hypatio/settings.py index 8059b5bf..edb0bb60 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -12,7 +12,7 @@ import os -from os.path import normpath, join +from os.path import normpath, join, dirname, abspath # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 24eae26c8a9320b4ebeba3891b20ddba3166f414 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 10:59:08 -0400 Subject: [PATCH 006/613] Forgot WSGI settings name change. --- hypatio/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypatio/settings.py b/hypatio/settings.py index edb0bb60..d490640a 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -74,7 +74,7 @@ }, ] -WSGI_APPLICATION = 'hypatio.wsgi.application' +WSGI_APPLICATION = 'hypatio-app.wsgi.application' # Database From ab8a2811932745d5c72ac9e935ac8ffc64c09279 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 18 Oct 2016 12:07:08 -0400 Subject: [PATCH 007/613] Reversing WSGI app name. --- hypatio/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypatio/settings.py b/hypatio/settings.py index d490640a..edb0bb60 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -74,7 +74,7 @@ }, ] -WSGI_APPLICATION = 'hypatio-app.wsgi.application' +WSGI_APPLICATION = 'hypatio.wsgi.application' # Database From 840910f85e71a6d2ff062267d1e580818bd4fa33 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 25 Oct 2016 07:46:02 -0400 Subject: [PATCH 008/613] Adding configurable URL. --- dataprojects/fixtures/dataprojects.json | 6 +- dataprojects/views.py | 2 +- db.sqlite3 | Bin 56320 -> 60416 bytes hypatio/settings.py | 3 +- hypatio/urls.py | 1 - templates/dataprojects/list.html | 4 +- tokenlogin/__init__.py | 0 .../__pycache__/__init__.cpython-34.pyc | Bin 151 -> 0 bytes .../auth0authenticate.cpython-34.pyc | Bin 1158 -> 0 bytes tokenlogin/__pycache__/urls.cpython-34.pyc | Bin 500 -> 0 bytes tokenlogin/__pycache__/views.cpython-34.pyc | Bin 2447 -> 0 bytes tokenlogin/admin.py | 3 - tokenlogin/apps.py | 5 -- tokenlogin/auth0authenticate.py | 28 --------- tokenlogin/migrations/__init__.py | 0 tokenlogin/models.py | 3 - tokenlogin/tests.py | 3 - tokenlogin/urls.py | 11 ---- tokenlogin/views.py | 58 ------------------ 19 files changed, 8 insertions(+), 119 deletions(-) delete mode 100644 tokenlogin/__init__.py delete mode 100644 tokenlogin/__pycache__/__init__.cpython-34.pyc delete mode 100644 tokenlogin/__pycache__/auth0authenticate.cpython-34.pyc delete mode 100644 tokenlogin/__pycache__/urls.cpython-34.pyc delete mode 100644 tokenlogin/__pycache__/views.cpython-34.pyc delete mode 100644 tokenlogin/admin.py delete mode 100644 tokenlogin/apps.py delete mode 100644 tokenlogin/auth0authenticate.py delete mode 100644 tokenlogin/migrations/__init__.py delete mode 100644 tokenlogin/models.py delete mode 100644 tokenlogin/tests.py delete mode 100644 tokenlogin/urls.py delete mode 100644 tokenlogin/views.py diff --git a/dataprojects/fixtures/dataprojects.json b/dataprojects/fixtures/dataprojects.json index 21495359..cf8f65ec 100644 --- a/dataprojects/fixtures/dataprojects.json +++ b/dataprojects/fixtures/dataprojects.json @@ -5,7 +5,7 @@ "fields": { "name": "Dr. Berson's Dataset", "institution": "", - "description": "A dataset collected over a lifetime of work.", + "description": "
A dataset collected over a lifetime of work.

", "short_description": "Biostats ready longitudinal dataset." } }, @@ -25,8 +25,8 @@ "fields": { "name": "GRDR", "institution": "HMS", - "description": "
An accumulation of data on rare disease.

", - "short_description": "An instance of i2b2/tranSMART loaded with data from the Global Rare Disease Registry Program." + "description": "
The integration of clinical and biomedical data hosted in multiple distributed repositories is confronted by two significant challenges: i) correctly linking information pertaining to the same patient across repositories, for example, linking lab results data with bedside observations data; and ii) making data available for analysis at different locations across a collaboration network. These problems are exacerbated in the case of rare diseases research, given the very limited availability of data sets and data standards. We propose to develop the NCAT Global Repository for Rare Diseases Research (GRDR) based on BD2K PIC-SURE platform to address these challenges. NCAT GRDR repository will be a scalable, secure, and flexible integration architecture for clinical and biomedical datasets, which by extending the successful i2b2/tranSMART platform will allow data providers to easily share their data with the wider research community without requiring them to subscribe to proprietary vocabulary standards or to develop complex mapping protocols. Using federated data access and querying methods that retrieve relevant data from different locations before combining them, GRDR will make it possible for comparative analysis methods to be executed on the integrated datasets. By assigning generic identifiers (after de-identification) to related data across locations, GRDR will ease the difficulties of linking data while conforming to the requirements of patient data privacy and other security regulations. We aim to progressively integrate up to 17 multidimensional, and heterogeneous patient registries on GRDR and provide encrypted access to this data to authenticated users over a secure connection.

", + "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository." } } ] \ No newline at end of file diff --git a/dataprojects/views.py b/dataprojects/views.py index fb87c8e9..568622ec 100644 --- a/dataprojects/views.py +++ b/dataprojects/views.py @@ -37,4 +37,4 @@ def listDataprojects(request, template_name='dataprojects/list.html'): all_data_projects = DataProject.objects.all() - return render(request, template_name, {"dataprojects": all_data_projects, "user_logged_in": user_logged_in, "user": user}) + return render(request, template_name, {"dataprojects": all_data_projects, "user_logged_in": user_logged_in, "user": user, "account_server_url": settings.ACCOUNT_SERVER_URL}) diff --git a/db.sqlite3 b/db.sqlite3 index 1e4d3d63b63f47dd491e71d9632efe55a664853c..24944267ee4933b875953eefdde490474367a4e3 100644 GIT binary patch delta 2700 zcmd5;%a0pL9d_G_Hj4<_1aVp^TVZ+Zu+B`}^PYr2CX~i6>*#W_S078f(0*NCRPAlz^AjJ18KN22-D>u8{RrS4o zU-_M1mfrb8>3gr92nvP5#YgOGR`B#&AG~?;mC}cJP}mrMx_q`+`fz;i|@$c5}OW!ZPYdy7|mHt%xS@EEBv-rm%vmTbB((9#P9G^Ss zT3`G^sb+=tuphds!rnY=+csZcYxm>L<+bj{ioG#-;1A9mKdpTH@)>)DRW`ROD_fP# z#f>$4b^XHeM;Gg_lulcpj9Ou>ND`JRmIh+Fj`@>SiYYIUs|U5}ihH*l5?y18@E$!m>zP>Xi$R^!%zbI@FIhP!stcDl93HQO0pm(H+x zp|xL=5ETD?|dSKf*1x!<_AIsVOipCA1vFUh?Jnbb1wkoXaqkYsM+Y3U03($lJ^ zrAWEXSzmeF%|iMJBVwOxKVeGKYrg1(m-MBG~g-BPH}!;;6o_-JWeO;+FC`-m+iLD0?$G~OQnhv z=8Cq`f`GU(p3y3^8}-pnoeg9d0$W!wnX;Zr66uD5VSxe8flJ~fjzr~&Or8T#*p`Vy z;bjC|#U`jSPDSTA6+|2?w{ym^ZO0tl5wXzHW6}r4k!(wh1bc(GQ=y^QdGi=gC(sv2 zN059`8x4rhxe<#Q34xI^C`be8Wnn4>%xJSv#$BRK^TPV2y3pg4+*8j~TT(UV$4 zz-3nTz?^h6t6+GadmsW+6jGsC0_Iw#Iq-E50HgFGYd|V#A_JQ{8Z!*_!ko>8VOftz zRn}RQg`-sZ5(g%*iHBvgdmtc%BAHZ{uo(y(A`@~Q(BcVaf`UW(kc^4g(|M1Kc(FKO zmjRFScm~jMO1k2uEFslZ|5fnd6DCHISa`$(1tTw`lD5Wg|G586r+RbAxq4-PkKG8B zi(;vBglR`6q<6yJn#xq4AOuz0wU<)O15$^pZ$?MdXrLiv$kgn`;xr8k5Zml3c)C3 z9#RH*Lt~;VRU~}#)i@BKrlGrx$@NsC4=||Yr)#E1&*UM0;Tok=s}Y9DXKv2Y0UC(r zhvou_^a37+BJPM}i^&DNtu>58%4R{A6z<4mlJ$g6`E=|jK~H!j=E35g8K@T#l+E;u z!x5+AD$jQfxr?wTATZ6O06Ru_9my1j+!NY0L5JYTJ@cp;;E|@^ zA^aq{;3By8f->u1NGEUh(YGKRNk~6pQ?N3mImdeAC6$_?Hu*MdeKY$2f}opQVpjij zK8??OdS+hdM&^GWruq~Tdjo+R)fboxckFMlx2jjq@7=9mV?7iOSxi;JeV=-U zh~nhu*;t$&OZ_;vH=p!9svC2Uw2~9$X#-69r;WSHTmh2uM`ccelTOgYOEV~FE{FdT zcT%WKMe89RISfffjQ=F&I>l9L{@S#&@L!u^PRd+KJbXJ;gIW8UnnP!SFU*t3dJ>SC lAT4omm|~|ilG+2rQBzWlyGTwHWidHOc~u$RpBVGae*ng!wxR$4 delta 257 zcmZp;!Q8Ned4e=+3IhX!`9uYK#*~c-OSC!onYRK37|bX0=*Uf0k%7sT=^x`Y=1ENbj2jt$Gv8s_!j#4A!E}>JfjO61nR)wWMUGC!%^MBh za|#)nD;Szv8Jk%dnd@1ao0%GzZQf)ZB)}}dBsiJfN0EtvVY8Oc3C79t((;=H{oV)i z@dB-8W_ZfL+{a|d@DynOb(SViW{%C#PnR;u6fm+enDVA3x)kXtIHeX9=jW*xE4U<< zBo?QZOzv2qqve#DUtE${QmjyvnwV0lkdvR6o>@|wl9`v7qfiWFR4SxERO>BTVgUf$ C5Kk2V diff --git a/hypatio/settings.py b/hypatio/settings.py index edb0bb60..b011cd7b 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -113,7 +113,8 @@ AUTH0_SUCCESS_URL = os.environ["AUTH0_SUCCESS_URL"] AUTH0_LOGOUT_URL = os.environ["AUTH0_LOGOUT_URL"] -AUTHENTICATION_BACKENDS = ('tokenlogin.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend') +AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend') +ACCOUNT_SERVER_URL = "http://authentication.aws.dbmi.hms.harvard.edu:8001/login/auth/" # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ diff --git a/hypatio/urls.py b/hypatio/urls.py index eaa4aac9..01728d34 100644 --- a/hypatio/urls.py +++ b/hypatio/urls.py @@ -19,5 +19,4 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^dataprojects/', include('dataprojects.urls')), - url(r'^tokenlogin/', include('tokenlogin.urls')), ] diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html index e693ed84..d8468efa 100644 --- a/templates/dataprojects/list.html +++ b/templates/dataprojects/list.html @@ -54,7 +54,7 @@ @@ -63,7 +63,7 @@ {% else %}
  • - +   Register / Sign In diff --git a/tokenlogin/__init__.py b/tokenlogin/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tokenlogin/__pycache__/__init__.cpython-34.pyc b/tokenlogin/__pycache__/__init__.cpython-34.pyc deleted file mode 100644 index 08fba12ede5020aa759f13c182bedb48343ac246..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 151 zcmaFI!^`zU{&6@15IhDEFu(|8H~?`m3y?@*UcHR zQ%cj)GE?>S$})>eN)vNZ^U8|#Gb#%bOEUA}bV+`8YFHV%6n|$spSW%KC{+=aA_jydNDvDPLam@M09gtugu!y`OX}1_C-n%3a!6ol^AMft>KK;YrXg>M!^Z2F<@D0|6hwFV5b%>DQpFswY zGw3s5w09tLpzi>8VC(|-V2-*9JWX6kJotz=Jm6KByU?%E?W3Ub9jPE-#(Sl>?8UTO z4F?zdDC!mh2eN`jkRI*gW2EaN?~1zbK~h1+s=0hn>XW+!T%No>Fjb${QW zqf6pBIQSZUJa-_ht`;@gj_WW7U~dG2*SN(Xdb1k`TJwCOQ!#4mqP-RlZ&R+?ciN`H z^C-uqZ`xyB$l8@rzOCWzaCytQd{BG?j!8%8{h}np!a!^H?k6 zj<`1dn`%H1O+1mwZy)JdcxdI81rBbCyb7Mj01ps(Uh>P=Wr;mJIz&MK_`{5-iKeJCmvL z*AMDc#48BEwjG!4u@>8K_E-adTWn@?_D>&l)}P0e0U9)4KzW3s?juZhmVGg{-G&v! z+8-T@U93Fzi)$-?k1CHHd1UKGi@@m_XL1^NHn)=8lMR9}>%nYY%$Y7kDl8O ivmr>LGu1_ZS++Hs)`dmb4;KDDq%Ty_n-=rgmirqPZW6Ho diff --git a/tokenlogin/__pycache__/urls.cpython-34.pyc b/tokenlogin/__pycache__/urls.cpython-34.pyc deleted file mode 100644 index c05ce4e3d95243160914d05c38afdc9f05ed2a6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmYjN%T59@6g_30z@U5t3)wI~U<@B%+!$jriy;$w!O>}lv;zhfZu|;A!C&gmxME}C z)|D*?(#N^y=019Qzmi_(Uk;6+1^paOGVShw6(7PLY-a4gx8WxQWcVyaz0rUbK0EcCoR&! zCdeCQO_Cprt=G%{>#cj9pSDN`H^a8+^T$HY>(R89vyoM(Rm(~=!K$QZt$fQ>;2?D1 z=Bi=~rfX9dMbnUy`W-590S7- cnYzbEi?xX_Ma`ELUAb9l*R;|7C}O(o7G=ecYjq~Rs3guCHLLm|K0m3 z1MnltKg~NC%{aAp9C{GM-$+sz)ypj20sI4W{xJo z%YwfI=F)te0xt(O~QbY_~HFgd!ePoSw=W_gCWy#!=yFkBYda4 zaOeiMBF@1lg>41fJ~pNzdIK=VzXI<7XiW}P7$?MC@Fa{=@J@xdD5v2Dbrdkff{fHq zg^$Fv3bQA`voKzQ8x?pC#>;Xsx+5VJoTRXJh)zsPd<8-EZ3241Cy*5DR^d$!==Y!~ z3SR>!BM&+c6gay{NJ{9pEIj8MFfIT>e*<`%gqtMLfBiP&2Vx3!kbv?teX&2%L<4j1 z!DC@iX<3hx0r`fT*~W{O)<5cXB6YKG<}7a*QkRc>W~L5}`p&MIwWf|s0-Hn+UWJUg z1CJC3mQ&>68xoYw%!Gy)u1BH`!#F7(pB!j<@zvfzv$$D|Q^dDiuk&soEQ>LF-;)e*z0xBez$#0lLF7|pPanZnns)$U-LNa z5Ib~8oE}(=>~69#0qOl^UDT1v#J30lnKWUJZPlddc&^h4ZQ z+C1+*!C|~C?U|GRGXgA)9jG~^N&ke>9Z5kjflZR!MB$Dk<|ER=e8lv6j>#pYB<V zsAHyK#2clw9&L`WYr^1$~}kP<5IB(=$046qJ$R7?5Il4_S_Tq z=;iC$se8PCG#VV(Vytx|t){z2&&sGYvA0?R44uRFwH<|5$Msru(9_>IJ=3>(-Rb$+ z>#Eh*o1WL7@#C4sv_P+&yV|biJ6D6c!%U-gHP<$JOb-R+FR*WQZ*qkGi0yG=pd zBZp|nSnPee)#})@*7%%hzBh9AwcS?dbW3lXZ?`(yOqXX&uEiOo)2!R|Z+1yAY1(7& z(x{KVHV$>G=hu#VXFBcn+AodzGrjFs-HX%mPF=5&=;hYaZnRUo=snqO)Hj36XsC5A z{fHaxWjJx8z%b8ePvez6%zG>>k%;oZT<$cD&S7(}(QF&h$NzUXi_|KQxbKxzDx!so zus~Uam$|Xf=+oN|#IGZ6b$-hWz0YwK%VGzKypj{S@fVcu!tNKjk;9q0uro?3CP5<% zND)&HzbeC`8+{EDT@k8Egn)v1)q3*Uh2vWuZ7!F&ZL9q_4c+5qWk$o z8Y*vKQ60;67+lN?c5=+HYOdaHg*aBg0Tq6xABaO9{Bou&Dypocv> Date: Tue, 25 Oct 2016 08:59:46 -0400 Subject: [PATCH 009/613] Adding custom login class. --- hypatio/auth0authenticate.py | 28 ++++++++++++++++++++++++++++ hypatio/settings.py | 26 +++++++++++++++++--------- 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 hypatio/auth0authenticate.py diff --git a/hypatio/auth0authenticate.py b/hypatio/auth0authenticate.py new file mode 100644 index 00000000..98fe1d77 --- /dev/null +++ b/hypatio/auth0authenticate.py @@ -0,0 +1,28 @@ +from django.contrib.auth.models import User +import logging +logger = logging.getLogger(__name__) + + +class Auth0Authentication(object): + + def authenticate(self, **token_dictionary): + logger.debug("Attempting to Authenticate User - " + token_dictionary["email"]) + + try: + user = User.objects.get(username=token_dictionary["email"]) + except User.DoesNotExist: + logger.debug("User not found, creating.") + + user = User(username=token_dictionary["email"], email=token_dictionary["email"]) + user.is_staff = True + user.is_superuser = True + user.save() + return user + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + diff --git a/hypatio/settings.py b/hypatio/settings.py index b011cd7b..247d0665 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -13,6 +13,9 @@ import os from os.path import normpath, join, dirname, abspath +from django.utils.crypto import get_random_string + +chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -22,7 +25,7 @@ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ["SECRET_KEY"] +SECRET_KEY = os.environ.get("SECRET_KEY", get_random_string(50, chars)) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -106,14 +109,14 @@ }, ] -AUTH0_DOMAIN = os.environ["AUTH0_DOMAIN"] -AUTH0_CLIENT_ID = os.environ["AUTH0_CLIENT_ID"] -AUTH0_SECRET = os.environ["AUTH0_SECRET"] -AUTH0_CALLBACK_URL = os.environ["AUTH0_CALLBACK_URL"] -AUTH0_SUCCESS_URL = os.environ["AUTH0_SUCCESS_URL"] -AUTH0_LOGOUT_URL = os.environ["AUTH0_LOGOUT_URL"] +AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") +AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") +AUTH0_SECRET = os.environ.get("AUTH0_SECRET") +AUTH0_CALLBACK_URL = os.environ.get("AUTH0_CALLBACK_URL") +AUTH0_SUCCESS_URL = os.environ.get("AUTH0_SUCCESS_URL") +AUTH0_LOGOUT_URL = os.environ.get("AUTH0_LOGOUT_URL") -AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend') +AUTHENTICATION_BACKENDS = ('hypatio.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend') ACCOUNT_SERVER_URL = "http://authentication.aws.dbmi.hms.harvard.edu:8001/login/auth/" # Internationalization @@ -154,4 +157,9 @@ STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -) \ No newline at end of file +) + +try: + from .local_settings import * +except ImportError: + pass From 288a7b0b6ca907fad44872cacc55ecb0f15c1e9a Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 25 Oct 2016 13:44:37 -0400 Subject: [PATCH 010/613] Updating examples. --- dataprojects/fixtures/dataprojects.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dataprojects/fixtures/dataprojects.json b/dataprojects/fixtures/dataprojects.json index cf8f65ec..5cc00904 100644 --- a/dataprojects/fixtures/dataprojects.json +++ b/dataprojects/fixtures/dataprojects.json @@ -3,10 +3,10 @@ "model": "dataprojects.dataproject", "pk": 1, "fields": { - "name": "Dr. Berson's Dataset", + "name": "Eliot L. Berson Database", "institution": "", - "description": "
    A dataset collected over a lifetime of work.

    ", - "short_description": "Biostats ready longitudinal dataset." + "description": "

    Trial 1


    601 adults with typical RP were treated with either vitamin A palmitate 15,000 IU per day on average or vitamin E 400 IU per day to assess impact on cone electroretinograms (ERGs), an established predicctor of disease progression.

    Berson EL, Rosner B, Sandberg MA, Hayes KC, Nicholson BW, Weigel DiFranco C, Willet W. A randomized trial of viatamin A and vitamin E supplementation for retinitis pigmentosa. Arch Ophthalmol. 111:761-772; 1993.

    Trial 2


    221 adults with typical RP were treated either with 1200 mg DHA (docosahexaenoic acid) capsules per day versus control capsules.
    More references.
    Click here to request access to the Eliot L. Berson Database.", + "short_description": "Three Clinical Trials of Nutritional Supplements for Retinitis Pigmentosa" } }, { @@ -15,8 +15,8 @@ "fields": { "name": "NHANES", "institution": "HMS", - "description": "
    A detailed database of health information for the US Population.

    ", - "short_description": "An instance of i2b2/tranSMART loaded with the publicly available NHANES dataset." + "description": "
    The National Health and Nutrition Examination Survey (NHANES) is a population survey implemented by the Centers for Disease Control and Prevention (CDC) to monitor the health of the United States whose data is publicly available in hundreds of files. This Data Descriptor describes a single unified and universally accessible data file, merging across 255 separate files and stitching data across 4 surveys, encompassing 41,474 individuals and 1,191 variables. The variables consist of phenotype and environmental exposure information on each individual, specifically

    • Demographic information
    • Physical exam results (e.g., height, body mass index)
    • Laboratory results (e.g., cholesterol, glucose, and environmental exposures)
    • Questionnaire items
    Second, the data descriptor describes a dictionary to enable analysts find variables by category and human-readable description.

    The datasets are available on DataDryad and a hands-on analytics tutorial is available on GitHub. Through a new big data platform, BD2K Patient Centered Information Commons (http://pic-sure.org), we provide a new way to browse the dataset via a web browser and provide application programming interface for programmatic access.

    ", + "short_description": "A database of human exposomes and phenomes from the US National Health and Nutrition Examination Survey" } }, { @@ -25,8 +25,8 @@ "fields": { "name": "GRDR", "institution": "HMS", - "description": "
    The integration of clinical and biomedical data hosted in multiple distributed repositories is confronted by two significant challenges: i) correctly linking information pertaining to the same patient across repositories, for example, linking lab results data with bedside observations data; and ii) making data available for analysis at different locations across a collaboration network. These problems are exacerbated in the case of rare diseases research, given the very limited availability of data sets and data standards. We propose to develop the NCAT Global Repository for Rare Diseases Research (GRDR) based on BD2K PIC-SURE platform to address these challenges. NCAT GRDR repository will be a scalable, secure, and flexible integration architecture for clinical and biomedical datasets, which by extending the successful i2b2/tranSMART platform will allow data providers to easily share their data with the wider research community without requiring them to subscribe to proprietary vocabulary standards or to develop complex mapping protocols. Using federated data access and querying methods that retrieve relevant data from different locations before combining them, GRDR will make it possible for comparative analysis methods to be executed on the integrated datasets. By assigning generic identifiers (after de-identification) to related data across locations, GRDR will ease the difficulties of linking data while conforming to the requirements of patient data privacy and other security regulations. We aim to progressively integrate up to 17 multidimensional, and heterogeneous patient registries on GRDR and provide encrypted access to this data to authenticated users over a secure connection.

    ", - "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository." + "description": "
    The integration of clinical and biomedical data hosted in multiple distributed repositories is confronted by two significant challenges: i) correctly linking information pertaining to the same patient across repositories, for example, linking lab results data with bedside observations data; and ii) making data available for analysis at different locations across a collaboration network. These problems are exacerbated in the case of rare diseases research, given the very limited availability of data sets and data standards.

    We propose to develop the NCAT Global Repository for Rare Diseases Research (GRDR) based on BD2K PIC-SURE platform to address these challenges. NCAT GRDR repository will be a scalable, secure, and flexible integration architecture for clinical and biomedical datasets, which by extending the successful i2b2/tranSMART platform will allow data providers to easily share their data with the wider research community without requiring them to subscribe to proprietary vocabulary standards or to develop complex mapping protocols. Using federated data access and querying methods that retrieve relevant data from different locations before combining them, GRDR will make it possible for comparative analysis methods to be executed on the integrated datasets. By assigning generic identifiers (after de-identification) to related data across locations, GRDR will ease the difficulties of linking data while conforming to the requirements of patient data privacy and other security regulations.

    ", + "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository" } } ] \ No newline at end of file From f4b67a5d1c7219fd2f2133deaefc7ec842a6645e Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Fri, 11 Nov 2016 17:20:43 -0500 Subject: [PATCH 011/613] Implementing security stuff. --- dataprojects/fixtures/dataprojects.json | 25 +++++++++++------ .../0002_dataproject_project_key.py | 20 ++++++++++++++ .../0003_dataproject_permission_scheme.py | 20 ++++++++++++++ .../0004_dataproject_project_url.py | 20 ++++++++++++++ ...002_dataproject_project_key.cpython-34.pyc | Bin 0 -> 777 bytes ...taproject_permission_scheme.cpython-34.pyc | Bin 0 -> 802 bytes ...004_dataproject_project_url.cpython-34.pyc | Bin 0 -> 790 bytes dataprojects/models.py | 5 +++- dataprojects/templatetags/__init__.py | 0 .../__pycache__/__init__.cpython-34.pyc | Bin 0 -> 166 bytes .../dataprojects_extras.cpython-34.pyc | Bin 0 -> 398 bytes .../templatetags/dataprojects_extras.py | 9 ++++++ dataprojects/views.py | 26 ++++++++++++++++-- templates/dataprojects/blurb.html | 14 ++++++++++ templates/dataprojects/list.html | 4 +++ 15 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 dataprojects/migrations/0002_dataproject_project_key.py create mode 100644 dataprojects/migrations/0003_dataproject_permission_scheme.py create mode 100644 dataprojects/migrations/0004_dataproject_project_url.py create mode 100644 dataprojects/migrations/__pycache__/0002_dataproject_project_key.cpython-34.pyc create mode 100644 dataprojects/migrations/__pycache__/0003_dataproject_permission_scheme.cpython-34.pyc create mode 100644 dataprojects/migrations/__pycache__/0004_dataproject_project_url.cpython-34.pyc create mode 100644 dataprojects/templatetags/__init__.py create mode 100644 dataprojects/templatetags/__pycache__/__init__.cpython-34.pyc create mode 100644 dataprojects/templatetags/__pycache__/dataprojects_extras.cpython-34.pyc create mode 100644 dataprojects/templatetags/dataprojects_extras.py diff --git a/dataprojects/fixtures/dataprojects.json b/dataprojects/fixtures/dataprojects.json index 5cc00904..6ff8bee4 100644 --- a/dataprojects/fixtures/dataprojects.json +++ b/dataprojects/fixtures/dataprojects.json @@ -5,28 +5,37 @@ "fields": { "name": "Eliot L. Berson Database", "institution": "", - "description": "

    Trial 1


    601 adults with typical RP were treated with either vitamin A palmitate 15,000 IU per day on average or vitamin E 400 IU per day to assess impact on cone electroretinograms (ERGs), an established predicctor of disease progression.

    Berson EL, Rosner B, Sandberg MA, Hayes KC, Nicholson BW, Weigel DiFranco C, Willet W. A randomized trial of viatamin A and vitamin E supplementation for retinitis pigmentosa. Arch Ophthalmol. 111:761-772; 1993.

    Trial 2


    221 adults with typical RP were treated either with 1200 mg DHA (docosahexaenoic acid) capsules per day versus control capsules.
    More references.
    Click here to request access to the Eliot L. Berson Database.", - "short_description": "Three Clinical Trials of Nutritional Supplements for Retinitis Pigmentosa" + "description": "

    Trial 1


    601 adults with typical RP were treated with either vitamin A palmitate 15,000 IU per day on average or vitamin E 400 IU per day to assess impact on cone electroretinograms (ERGs), an established predicctor of disease progression.

    Berson EL, Rosner B, Sandberg MA, Hayes KC, Nicholson BW, Weigel DiFranco C, Willet W. A randomized trial of viatamin A and vitamin E supplementation for retinitis pigmentosa. Arch Ophthalmol. 111:761-772; 1993.

    Trial 2


    221 adults with typical RP were treated either with 1200 mg DHA (docosahexaenoic acid) capsules per day versus control capsules.
    More references.
    ", + "short_description": "Three Clinical Trials of Nutritional Supplements for Retinitis Pigmentosa", + "project_key": "BERSON", + "permission_scheme": "REQUEST_ACCESS", + "project_url": "" } }, { "model": "dataprojects.dataproject", "pk": 2, "fields": { - "name": "NHANES", + "name": "NHANES i2b2/tranSMART", "institution": "HMS", - "description": "
    The National Health and Nutrition Examination Survey (NHANES) is a population survey implemented by the Centers for Disease Control and Prevention (CDC) to monitor the health of the United States whose data is publicly available in hundreds of files. This Data Descriptor describes a single unified and universally accessible data file, merging across 255 separate files and stitching data across 4 surveys, encompassing 41,474 individuals and 1,191 variables. The variables consist of phenotype and environmental exposure information on each individual, specifically

    • Demographic information
    • Physical exam results (e.g., height, body mass index)
    • Laboratory results (e.g., cholesterol, glucose, and environmental exposures)
    • Questionnaire items
    Second, the data descriptor describes a dictionary to enable analysts find variables by category and human-readable description.

    The datasets are available on DataDryad and a hands-on analytics tutorial is available on GitHub. Through a new big data platform, BD2K Patient Centered Information Commons (http://pic-sure.org), we provide a new way to browse the dataset via a web browser and provide application programming interface for programmatic access.

    ", - "short_description": "A database of human exposomes and phenomes from the US National Health and Nutrition Examination Survey" + "description": "
    The National Health and Nutrition Examination Survey (NHANES) is a population survey implemented by the Centers for Disease Control and Prevention (CDC) to monitor the health of the United States whose data is publicly available in hundreds of files. This Data Descriptor describes a single unified and universally accessible data file, merging across 255 separate files and stitching data across 4 surveys, encompassing 41,474 individuals and 1,191 variables. The variables consist of phenotype and environmental exposure information on each individual, specifically

    • Demographic information
    • Physical exam results (e.g., height, body mass index)
    • Laboratory results (e.g., cholesterol, glucose, and environmental exposures)
    • Questionnaire items
    Second, the data descriptor describes a dictionary to enable analysts find variables by category and human-readable description.

    The datasets are available on DataDryad and a hands-on analytics tutorial is available on GitHub. Through a new big data platform, BD2K Patient Centered Information Commons (http://pic-sure.org), we provide a new way to browse the dataset via a web browser and provide application programming interface for programmatic access.

    ", + "short_description": "A database of human exposomes and phenomes from the US National Health and Nutrition Examination Survey", + "project_key": "NHANES", + "permission_scheme": "PUBLIC", + "project_url": "https://nhanes.hms.harvard.edu/transmart/datasetExplorer/index" } }, { "model": "dataprojects.dataproject", "pk": 3, "fields": { - "name": "GRDR", + "name": "GRDR i2b2/tranSMART", "institution": "HMS", - "description": "
    The integration of clinical and biomedical data hosted in multiple distributed repositories is confronted by two significant challenges: i) correctly linking information pertaining to the same patient across repositories, for example, linking lab results data with bedside observations data; and ii) making data available for analysis at different locations across a collaboration network. These problems are exacerbated in the case of rare diseases research, given the very limited availability of data sets and data standards.

    We propose to develop the NCAT Global Repository for Rare Diseases Research (GRDR) based on BD2K PIC-SURE platform to address these challenges. NCAT GRDR repository will be a scalable, secure, and flexible integration architecture for clinical and biomedical datasets, which by extending the successful i2b2/tranSMART platform will allow data providers to easily share their data with the wider research community without requiring them to subscribe to proprietary vocabulary standards or to develop complex mapping protocols. Using federated data access and querying methods that retrieve relevant data from different locations before combining them, GRDR will make it possible for comparative analysis methods to be executed on the integrated datasets. By assigning generic identifiers (after de-identification) to related data across locations, GRDR will ease the difficulties of linking data while conforming to the requirements of patient data privacy and other security regulations.

    ", - "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository" + "description": "
    The integration of clinical and biomedical data hosted in multiple distributed repositories is confronted by two significant challenges: i) correctly linking information pertaining to the same patient across repositories, for example, linking lab results data with bedside observations data; and ii) making data available for analysis at different locations across a collaboration network. These problems are exacerbated in the case of rare diseases research, given the very limited availability of data sets and data standards.

    We propose to develop the NCAT Global Repository for Rare Diseases Research (GRDR) based on BD2K PIC-SURE platform to address these challenges. NCAT GRDR repository will be a scalable, secure, and flexible integration architecture for clinical and biomedical datasets, which by extending the successful i2b2/tranSMART platform will allow data providers to easily share their data with the wider research community without requiring them to subscribe to proprietary vocabulary standards or to develop complex mapping protocols. Using federated data access and querying methods that retrieve relevant data from different locations before combining them, GRDR will make it possible for comparative analysis methods to be executed on the integrated datasets. By assigning generic identifiers (after de-identification) to related data across locations, GRDR will ease the difficulties of linking data while conforming to the requirements of patient data privacy and other security regulations.

    ", + "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository", + "project_key": "GRDR", + "permission_scheme": "PUBLIC", + "project_url": "https://grdr.hms.harvard.edu/transmart/datasetExplorer/index" } } ] \ No newline at end of file diff --git a/dataprojects/migrations/0002_dataproject_project_key.py b/dataprojects/migrations/0002_dataproject_project_key.py new file mode 100644 index 00000000..991310c6 --- /dev/null +++ b/dataprojects/migrations/0002_dataproject_project_key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-11-11 21:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dataprojects', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='project_key', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Project Key'), + ), + ] diff --git a/dataprojects/migrations/0003_dataproject_permission_scheme.py b/dataprojects/migrations/0003_dataproject_permission_scheme.py new file mode 100644 index 00000000..301f5358 --- /dev/null +++ b/dataprojects/migrations/0003_dataproject_permission_scheme.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-11-11 21:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dataprojects', '0002_dataproject_project_key'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='permission_scheme', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Permission Scheme'), + ), + ] diff --git a/dataprojects/migrations/0004_dataproject_project_url.py b/dataprojects/migrations/0004_dataproject_project_url.py new file mode 100644 index 00000000..135e7263 --- /dev/null +++ b/dataprojects/migrations/0004_dataproject_project_url.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-11-11 21:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dataprojects', '0003_dataproject_permission_scheme'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='project_url', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Project URL'), + ), + ] diff --git a/dataprojects/migrations/__pycache__/0002_dataproject_project_key.cpython-34.pyc b/dataprojects/migrations/__pycache__/0002_dataproject_project_key.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc3392db490ac3d4b4ab47fbbde3bcb871d6ef80 GIT binary patch literal 777 zcmY*WJ&)5s5FLMB?D)9QAn2%&+#~@}0y=aO6o?ZNAf3{zHknDzxwRd3?TDlee}=!b zE!8#jP%-Ne$l2cU%+9=>_hx@)r|HY*Pp*D>06*a93E4mAWiHqte;z1+(u0Wyo{N1@ zK9m7W0w_b6gy6l!05F7aoD6{iSVpkpd;}Q5$2`8}gCO_ppsk8ojU-gjke15izD>(w zp=DFlm9cTjNo8iP{P3KybI!|Lu_16In8U;e9KeD(1wf(ew`aipFa|sUieY&I*aJ)e zQ@}o8*7>J^_h1L%4gv?~f-|Og-zM)5JZy%tkt$G3zuTwY}r{pYI#{Pkb7^di(O`u?z4UjxL||3qJai1@hy71SlLBJK$K` z1?fWJ!PtYshp`XNC-eY)_{l*Z$b&@yTaE{S0bD1YJ8lGtWBN^*XBCo=c}*&nI&n=@ zUBEuf7?TI&+j%<#JP$j7U7!vux_~{v z5HJEf0gPMy6z~yj!QX;s%Q@kUAr4IVd5^=ynAYh^RSU{$ZJwW>pT8DIE^!|cOWH7e z3t5zDLF1F7B;#9`8LUVZxzqXnjXYlfHeRJFE=D(0~JK)*me$JqXHh=%@L}$Q^BO%sAHH$Y2l8MOz@GB~U;rPI-VGlFiDSl1nP(M}ka~Rln?B$G zFa(SMhk$YGp8(#0J@|X@Y&#c>7~)+MzCGYDF{X98Rn>;FTAN3++3blp+7eq*MXohN z7CKu~K}^B|7iC(|e0Wr9e7j|ae-9Ilk|tQ@B+nUHH^zVJ72lpIQ)>GE?>1|y*l2)b3_DTgjaKKAPY+^6;L#M}gyMLbaEX!q6 sH;Q>F{)ZVlZqjm9O>ogNYx%U*|FL)816w_K-f8s0s-P|Rv3t+^1C9vUTL1t6 literal 0 HcmV?d00001 diff --git a/dataprojects/models.py b/dataprojects/models.py index 818cea1a..0464105e 100644 --- a/dataprojects/models.py +++ b/dataprojects/models.py @@ -1,8 +1,11 @@ from django.db import models + class DataProject(models.Model): name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Name of project") institution = models.CharField(max_length=255, blank=True, null=True, verbose_name="Institution") description = models.CharField(max_length=4000, blank=True, null=True, verbose_name="Description") short_description = models.CharField(max_length=255, blank=True, null=True, verbose_name="Short Description") - + project_key = models.CharField(max_length=100, blank=True, null=True, verbose_name="Project Key") + permission_scheme = models.CharField(max_length=100, blank=True, null=True, verbose_name="Permission Scheme") + project_url = models.CharField(max_length=255, blank=True, null=True, verbose_name="Project URL") diff --git a/dataprojects/templatetags/__init__.py b/dataprojects/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dataprojects/templatetags/__pycache__/__init__.cpython-34.pyc b/dataprojects/templatetags/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c98c45e753188f8f2cbf48a605073cc231a49f2a GIT binary patch literal 166 zcmaFI!^^eBK`nv-2p)q77+?f49Dul(1xTbYFa&Ed`mJOr0tq9CU%~pJ#i>Qb`nkEu zDWz#?nW_4EWtl}KrHMJId1b}=8I=WzC7JnfIwi3rv7jhFD>b>KSidASw;(66B()?l iy;wg!J~J<~BtBlRpz;=nO>TZlX-=vg$g*M}W&i+pt}So? literal 0 HcmV?d00001 diff --git a/dataprojects/templatetags/__pycache__/dataprojects_extras.cpython-34.pyc b/dataprojects/templatetags/__pycache__/dataprojects_extras.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..006406f350595ab0ed15a37f668e9d1c7662aaa9 GIT binary patch literal 398 zcmYjLJ5Izf5FO`7gcb?Py#P|0O+|+gh!$q=d!46>U1Nu+~ zAUqI0ya4QIBLoq^DhWU66cT1*gLPMNgPG?#6iywk2|6ZvrerW*C2aWyiMKxKxYj7P zLS{y`4OgS}A8}O9)M^j4$NXBHD@y4- z^<#LU)Blq#J{iUJq8n{Y-wnL#F>)R! diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html index d8468efa..d8c16b7e 100644 --- a/templates/dataprojects/list.html +++ b/templates/dataprojects/list.html @@ -1,3 +1,5 @@ + + + + + \ No newline at end of file diff --git a/templates/dataprojects/blurb.html b/templates/dataprojects/blurb.html index 04e2b3e3..ec452a38 100644 --- a/templates/dataprojects/blurb.html +++ b/templates/dataprojects/blurb.html @@ -1,5 +1,7 @@ {% load dataprojects_extras %} + +
    - {% autoescape off %}{{ dataproject.description }}{% endautoescape %} + {% autoescape off %} + {{ dataproject.description }} + {% endautoescape %} - {% if dataproject.permission_scheme == "PUBLIC" %} + {% if project_permission_setup|keyvalue_permission_scheme:dataproject.project_key == "PUBLIC" %} - {% elif dataproject.permission_scheme == "REQUEST_ACCESS" %} + {% elif project_permission_setup|keyvalue_permission_scheme:dataproject.project_key == "PRIVATE" %} {% if permission_dictionary|keyvalue:dataproject.project_key == "VIEW" %} Click here to access the {{ dataproject.name }} {% else %} - Click here to request access to the {{ dataproject.name }} + {% if access_request_dictionary|permission_requested:dataproject.project_key %} + + You've already requested access to this project on {{ access_request_dictionary|get_date_requested:dataproject.project_key }}, please wait for your access to be granted. + +
    +
    + + {% else %} + Click here to request access to the {{ dataproject.name }} + {% endif %} + {% endif %} {% endif %}
    +
    + diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html index d8c16b7e..b01c876f 100644 --- a/templates/dataprojects/list.html +++ b/templates/dataprojects/list.html @@ -9,6 +9,22 @@ + + + + + +
  • - +   Register / Sign In diff --git a/templates/dataprojects/submit_request.html b/templates/dataprojects/submit_request.html new file mode 100644 index 00000000..d0707021 --- /dev/null +++ b/templates/dataprojects/submit_request.html @@ -0,0 +1,12 @@ + + + + + Submit Sent + + + +Thanks for submitting. + + + \ No newline at end of file From 99cdf092a6865ff4bc5e8f985b74547907557871 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 2 Jan 2017 14:13:18 -0500 Subject: [PATCH 014/613] TC-15 - Adding profile URL to Hypatio. --- dataprojects/views.py | 1 + hypatio/settings.py | 1 + templates/dataprojects/list.html | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dataprojects/views.py b/dataprojects/views.py index 65792e9e..2fe520a5 100644 --- a/dataprojects/views.py +++ b/dataprojects/views.py @@ -148,6 +148,7 @@ def listDataprojects(request, template_name='dataprojects/list.html'): "user_logged_in": user_logged_in, "user": user, "account_server_url": settings.ACCOUNT_SERVER_URL, + "profile_server_url": settings.SCIREG_SERVER_URL, "permission_dictionary": permission_dictionary, "project_permission_setup": project_permission_setup, "access_request_dictionary": access_request_dictionary}) diff --git a/hypatio/settings.py b/hypatio/settings.py index d67ddf1d..d18af6ad 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -117,6 +117,7 @@ AUTHENTICATION_BACKENDS = ('hypatio.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend') ACCOUNT_SERVER_URL = "http://authentication.aws.dbmi.hms.harvard.edu:8001/login/auth/" +SCIREG_SERVER_URL = "http://registration.aws.dbmi.hms.harvard.edu:8005" AUTHZ_BASE = "http://authorization.aws.dbmi.hms.harvard.edu:8003" PERMISSION_SERVER = AUTHZ_BASE + "/user_permission/" diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html index b01c876f..b71223e2 100644 --- a/templates/dataprojects/list.html +++ b/templates/dataprojects/list.html @@ -72,7 +72,7 @@ From f5b5b8b3e8cdee6dc109fe2de0831d95ca243fd6 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Wed, 4 Jan 2017 07:54:46 -0500 Subject: [PATCH 015/613] TC-16 - Signout URL. --- dataprojects/urls.py | 3 ++- dataprojects/views.py | 10 ++++++++-- hypatio/settings.py | 2 +- templates/dataprojects/list.html | 17 ++++------------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/dataprojects/urls.py b/dataprojects/urls.py index 655f0ae5..f0119b52 100644 --- a/dataprojects/urls.py +++ b/dataprojects/urls.py @@ -1,9 +1,10 @@ from django.conf.urls import url -from .views import listDataprojects, request_access, submit_request +from .views import listDataprojects, request_access, submit_request, signout urlpatterns = ( url(r'^$', listDataprojects), url(r'^list/$', listDataprojects), url(r'^request_access/$', request_access), url(r'^submit_request/$', submit_request), + url(r'^signout/$', signout), ) diff --git a/dataprojects/views.py b/dataprojects/views.py index 2fe520a5..1573b204 100644 --- a/dataprojects/views.py +++ b/dataprojects/views.py @@ -1,11 +1,11 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect from stronghold.decorators import public from dataprojects.models import DataProject from django.conf import settings import jwt import base64 from django.contrib import auth as django_auth -from django.contrib.auth import login +from django.contrib.auth import login, logout import logging import requests import json @@ -14,6 +14,12 @@ logger = logging.getLogger(__name__) +def signout(request): + logout(request) + response = redirect('/dataprojects/list') + response.delete_cookie('DBMI_JWT') + return response + def request_access(request, template_name='dataprojects/access_request.html'): user_jwt = request.COOKIES.get("DBMI_JWT", None) diff --git a/hypatio/settings.py b/hypatio/settings.py index d18af6ad..7d10f182 100644 --- a/hypatio/settings.py +++ b/hypatio/settings.py @@ -116,7 +116,7 @@ AUTH0_LOGOUT_URL = os.environ.get("AUTH0_LOGOUT_URL") AUTHENTICATION_BACKENDS = ('hypatio.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend') -ACCOUNT_SERVER_URL = "http://authentication.aws.dbmi.hms.harvard.edu:8001/login/auth/" +ACCOUNT_SERVER_URL = "http://authentication.aws.dbmi.hms.harvard.edu:8001/auth" SCIREG_SERVER_URL = "http://registration.aws.dbmi.hms.harvard.edu:8005" AUTHZ_BASE = "http://authorization.aws.dbmi.hms.harvard.edu:8003" diff --git a/templates/dataprojects/list.html b/templates/dataprojects/list.html index b71223e2..ec4fb640 100644 --- a/templates/dataprojects/list.html +++ b/templates/dataprojects/list.html @@ -40,28 +40,19 @@ -
    - +
    +
    {% modal_contact_form_link 'Need help? Contact us!' %}
    @@ -111,6 +210,8 @@

    Data Projects

    +{# Add a placeholder for the modal contact form #} + diff --git a/app/templates/email/base.html b/app/templates/email/base.html new file mode 100644 index 00000000..314a5660 --- /dev/null +++ b/app/templates/email/base.html @@ -0,0 +1,142 @@ + + + + + + {%block title%}{%endblock%} + + + + + + + + + +
      +
    + + + + + + + + +
    + + + + + + + + + + +
    + +
    + {%block content%}{%endblock%} +
    +

    Thank you!

    +
    +
    + + + + +
    +
     
    + + \ No newline at end of file diff --git a/app/templates/email/email_base.html b/app/templates/email/email_base.html new file mode 100644 index 00000000..795fd013 --- /dev/null +++ b/app/templates/email/email_base.html @@ -0,0 +1,131 @@ + + + + + + {%block title%}{%endblock%} + + + + + + + + + +
      +
    + + + + + + + + +
    + + + + + + + + + +
    +   +
    + {%block content%}{%endblock%} +
    +
    + + + + +
    +
     
    + + \ No newline at end of file diff --git a/app/templates/email/email_contact.html b/app/templates/email/email_contact.html new file mode 100644 index 00000000..c66d463a --- /dev/null +++ b/app/templates/email/email_contact.html @@ -0,0 +1,12 @@ +{% extends "email/email_base.html" %} + +{% block title %}PPM - Contact Form Submission{% endblock %} + +{% block content %} + +

    From: {{ from_name }}

    +

    Email: {{ from_email}}

    +

    Comment:

    +

    {{ message }}

    + +{% endblock %} diff --git a/app/templates/email/email_contact.txt b/app/templates/email/email_contact.txt new file mode 100644 index 00000000..34c03763 --- /dev/null +++ b/app/templates/email/email_contact.txt @@ -0,0 +1,4 @@ +Name: {{ from_name }} +Email: {{ from_email }} +Comment: +{{ message }} From e0e72145cbe3d637d314fae98f87cd26abe850ff Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Thu, 3 Aug 2017 15:38:28 -0400 Subject: [PATCH 031/613] Missed notify js file. --- app/static/bootstrap-notify.min.js | 1 + app/templates/dataprojects/list.html | 1 + 2 files changed, 2 insertions(+) create mode 100644 app/static/bootstrap-notify.min.js diff --git a/app/static/bootstrap-notify.min.js b/app/static/bootstrap-notify.min.js new file mode 100644 index 00000000..74f606ce --- /dev/null +++ b/app/static/bootstrap-notify.min.js @@ -0,0 +1 @@ +!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function s(s){var e=!1;return t('[data-notify="container"]').each(function(i,n){var a=t(n),o=a.find('[data-notify="title"]').text().trim(),r=a.find('[data-notify="message"]').html().trim(),l=o===t("
    "+s.settings.content.title+"
    ").html().trim(),d=r===t("
    "+s.settings.content.message+"
    ").html().trim(),g=a.hasClass("alert-"+s.settings.type);return l&&d&&g&&(e=!0),!e}),e}function e(e,n,a){var o={content:{message:"object"==typeof n?n.message:n,title:n.title?n.title:"",icon:n.icon?n.icon:"",url:n.url?n.url:"#",target:n.target?n.target:"-"}};a=t.extend(!0,{},o,a),this.settings=t.extend(!0,{},i,a),this._defaults=i,"-"===this.settings.content.target&&(this.settings.content.target=this.settings.url_target),this.animations={start:"webkitAnimationStart oanimationstart MSAnimationStart animationstart",end:"webkitAnimationEnd oanimationend MSAnimationEnd animationend"},"number"==typeof this.settings.offset&&(this.settings.offset={x:this.settings.offset,y:this.settings.offset}),(this.settings.allow_duplicates||!this.settings.allow_duplicates&&!s(this))&&this.init()}var i={element:"body",position:null,type:"info",allow_dismiss:!0,allow_duplicates:!0,newest_on_top:!1,showProgressbar:!1,placement:{from:"top",align:"right"},offset:20,spacing:10,z_index:1031,delay:5e3,timer:1e3,url_target:"_blank",mouse_over:null,animate:{enter:"animated fadeInDown",exit:"animated fadeOutUp"},onShow:null,onShown:null,onClose:null,onClosed:null,icon_type:"class",template:''};String.format=function(){for(var t=arguments[0],s=1;s .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[n],this.$ele.addClass("alert-"+i[n]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[n]);break;case"icon":var a=this.$ele.find('[data-notify="icon"]');"class"===t.settings.icon_type.toLowerCase()?a.removeClass(t.settings.content.icon).addClass(i[n]):(a.is("img")||a.find("img"),a.attr("src",i[n]));break;case"progress":var o=t.settings.delay-t.settings.delay*(i[n]/100);this.$ele.data("notify-delay",o),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[n]).css("width",i[n]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[n]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[n]);break;default:this.$ele.find('[data-notify="'+n+'"]').html(i[n])}var r=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(r)},close:function(){t.close()}}},buildNotify:function(){var s=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,s.title,s.message,s.url,s.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"===this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('Notify Icon')},styleDismiss:function(){this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:0,position:"absolute",top:0,width:"100%",zIndex:this.settings.z_index+1})},placement:function(){var s=this,e=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){e=Math.max(e,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),this.settings.newest_on_top===!0&&(e=this.settings.offset.y),i[this.settings.placement.from]=e+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit-","moz-","o-","ms-",""),function(t,e){s.$ele[0].style[e+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),this.settings.newest_on_top===!0&&(e=parseInt(e)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(e)),t.isFunction(s.settings.onShow)&&s.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){s.$ele.removeClass(s.settings.animate.enter),t.isFunction(s.settings.onShown)&&s.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(s.settings.onShown)&&s.settings.onShown.call(this)},600)},bind:function(){var s=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){s.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){s.$ele.data("notify-delay",s.settings.delay);var e=setInterval(function(){var t=parseInt(s.$ele.data("notify-delay"))-s.settings.timer;if("false"===s.$ele.data("data-hover")&&"pause"===s.settings.mouse_over||"pause"!=s.settings.mouse_over){var i=(s.settings.delay-t)/s.settings.delay*100;s.$ele.data("notify-delay",t),s.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-s.settings.timer&&(clearInterval(e),s.close())},s.settings.timer)}},close:function(){var s=this,e=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.attr("data-closing","true").addClass(this.settings.animate.exit),s.reposition(e),t.isFunction(s.settings.onClose)&&s.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(s.settings.onClosed)&&s.settings.onClosed.call(this)}),setTimeout(function(){i||(s.$ele.remove(),s.settings.onClosed&&s.settings.onClosed(s.$ele))},600)},reposition:function(s){var e=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);this.settings.newest_on_top===!0&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(e.settings.placement.from,s),s=parseInt(s)+parseInt(e.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(s){return i=t.extend(!0,{},i,s)},t.notifyClose=function(s){"warning"===s&&(s="danger"),"undefined"==typeof s||"all"===s?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):"success"===s||"info"===s||"warning"===s||"danger"===s?t(".alert-"+s+"[data-notify]").find('[data-notify="dismiss"]').trigger("click"):s?t(s+"[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+s+'"]').find('[data-notify="dismiss"]').trigger("click")},t.notifyCloseExcept=function(s){"warning"===s&&(s="danger"),"success"===s||"info"===s||"warning"===s||"danger"===s?t("[data-notify]").not(".alert-"+s).find('[data-notify="dismiss"]').trigger("click"):t("[data-notify]").not(s).find('[data-notify="dismiss"]').trigger("click")}}); diff --git a/app/templates/dataprojects/list.html b/app/templates/dataprojects/list.html index b66a348b..e95da9b9 100644 --- a/app/templates/dataprojects/list.html +++ b/app/templates/dataprojects/list.html @@ -13,6 +13,7 @@ + + diff --git a/app/templates/dataprojects/blurb.html b/app/templates/dataprojects/blurb.html index 8ba6a03a..102baefd 100644 --- a/app/templates/dataprojects/blurb.html +++ b/app/templates/dataprojects/blurb.html @@ -1,23 +1,21 @@ {% load dataprojects_extras %} - -
    {% autoescape off %} - {{ dataproject.description }} + {{ data_project.description }} {% endautoescape %} {% if not user_logged_in %} @@ -31,28 +29,20 @@

    Register / Sign In

    - {% else %} - {% if project_permission_setup|keyvalue_permission_scheme:dataproject.project_key == "PUBLIC" %} - - {% elif project_permission_setup|keyvalue_permission_scheme:dataproject.project_key == "PRIVATE" %} - {% if permission_dictionary|keyvalue:dataproject.project_key == "VIEW" %} - Click here to access the {{ dataproject.name }} - {% else %} - {% if access_request_dictionary|permission_requested:dataproject.project_key %} - - You've already requested access to this project on {{ access_request_dictionary|get_date_requested:dataproject.project_key }}, please wait for your access to be granted. - -
    -
    - - {% else %} - Click here to request access to the {{ dataproject.name }} - {% endif %} - - {% endif %} + {% else %} +
    + {% if data_project.permission_scheme == "PUBLIC" %} + Click here to access the {{ data_project.name }} + {% elif data_project.permission_scheme == "PRIVATE" and data_project.user_has_view_permissions %} + Click here to access the {{ data_project.name }} + {% elif data_project.permission_scheme == "PRIVATE" and data_project.user_requested_access %} + + You've already requested access to this project on {{ data_project.user_requested_access_on }}, please wait for your access to be granted. + + {% else %} + Click here to request access to the {{ data_project.name }} {% endif %} +
    {% endif %}
    diff --git a/app/templates/dataprojects/list.html b/app/templates/dataprojects/list.html index a6ba8b09..afd06fb3 100644 --- a/app/templates/dataprojects/list.html +++ b/app/templates/dataprojects/list.html @@ -194,9 +194,9 @@

    Data Projects

    - {% for dataproject in dataprojects %} - {% include "dataprojects/blurb.html" with project_counter=forloop.counter0 %} -
    + {% for data_project in data_projects %} + {% include "dataprojects/blurb.html" with project_counter=forloop.counter0 %} +
    {% endfor %}
    From b618bb54b61902d0505d99e85a5bba6c2f24ec70 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 19 Dec 2017 14:55:53 -0500 Subject: [PATCH 052/613] Date Requested was pointed to incorrect dict key --- app/dataprojects/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dataprojects/views.py b/app/dataprojects/views.py index 65e6ff49..3ed0ec79 100644 --- a/app/dataprojects/views.py +++ b/app/dataprojects/views.py @@ -105,7 +105,7 @@ def list_data_projects(request, template_name='dataprojects/list.html'): for access_request in user_access_requests: projects_with_access_requests[access_request['item']] = { - 'date_requested': access_request['date_request_granted'], + 'date_requested': access_request['date_requested'], 'request_granted': access_request['request_granted'], 'date_request_granted': access_request['date_request_granted']} From 1a4183fce7110a42b50eb042e765910713a8f874 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 19 Dec 2017 15:31:33 -0500 Subject: [PATCH 053/613] Added unit test to check for agreement form files existing Also puts the agreement form file path in a config setting. --- .../templatetags/dataprojects_extras.py | 3 +- app/dataprojects/tests.py | 41 ++++++++++++++++++- app/hypatio/settings.py | 3 ++ app/requirements.txt | 1 + 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/app/dataprojects/templatetags/dataprojects_extras.py b/app/dataprojects/templatetags/dataprojects_extras.py index f213a590..e89a9528 100644 --- a/app/dataprojects/templatetags/dataprojects_extras.py +++ b/app/dataprojects/templatetags/dataprojects_extras.py @@ -25,6 +25,5 @@ def modal_contact_form_link(text='Contact us', classes=''): @register.filter def get_dua_form_file_contents(dua_form_file): - form_path = settings.STATIC_ROOT + '/dua_forms/' + dua_form_file + form_path = os.path.join(settings.DATA_USE_AGREEMENT_FORM_DIR, dua_form_file) return open(form_path, 'r').read() - diff --git a/app/dataprojects/tests.py b/app/dataprojects/tests.py index 7ce503c2..6a50a6ba 100644 --- a/app/dataprojects/tests.py +++ b/app/dataprojects/tests.py @@ -1,3 +1,42 @@ +import os + +import mock +from mock import patch, Mock + from django.test import TestCase +from django.conf import settings +from django.core.management import call_command +from django.http import HttpResponse, HttpRequest +from django.contrib.auth import get_user_model + +from .models import DataUseAgreement, DataUseAgreementSign, DataProject +from .views import submit_request + +# TODO DELETE THIS +from .views import submit_request2 + +TEST_JWT_TOKEN = "WIUHDI&WHQDBKbIYWGD^GUQG^DG&wdydwg^@@Ejdh37364BQWKDBKWDU##B@@9wUDBi&@GiYWBD" +TEST_USER_EMAIL = "test-user@example.com" + +class HypatioTests(TestCase): + + def setUp(self): + # Get the fixtures inserted + call_command("loaddata", 'dataprojects', app_label='dataprojects') + + self.user = get_user_model().objects.create(email=TEST_USER_EMAIL, username=TEST_USER_EMAIL) + self.user.save() + + def test_every_dua_form_file_exists(self): + """ + Tests that every Data Use Agreement's agreement_form_file actually exists in our application. + This is important because right now there is no enforcement between the value entered in this + field and the existence of such a file on our server. + """ + + data_use_agreements = DataUseAgreement.objects.all() + + for dua in data_use_agreements: + form_path = os.path.join(settings.DATA_USE_AGREEMENT_FORM_DIR, dua.agreement_form_file) + self.assertEqual(True, os.path.isfile(form_path), "DUA '" + dua.name + "' agreement_form_file '" + dua.agreement_form_file + "' missing in directory " + settings.DATA_USE_AGREEMENT_FORM_DIR + ".") -# Create your tests here. diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 1576c222..7a783b1b 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -179,6 +179,9 @@ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) + +DATA_USE_AGREEMENT_FORM_DIR = STATIC_ROOT + '/dua_forms' + ########## EMAIL_BACKEND = 'django_smtp_ssl.SSLEmailBackend' diff --git a/app/requirements.txt b/app/requirements.txt index 0b5a422a..5fcb0900 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -5,6 +5,7 @@ django-stronghold==0.2.8 djangorestframework==3.5.3 djangorestframework-jwt==1.9.0 django-smtp-ssl==1.0 +mock==2.0.0 mysqlclient==1.3.9 py-auth0-jwt==0.2.5 py-auth0-jwt-rest==0.1 From 5021313f0254ac4a11dd691140cb32df9989f03b Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 19 Dec 2017 15:40:27 -0500 Subject: [PATCH 054/613] Removed unneeded references --- app/dataprojects/tests.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/dataprojects/tests.py b/app/dataprojects/tests.py index 6a50a6ba..f7d3a9a2 100644 --- a/app/dataprojects/tests.py +++ b/app/dataprojects/tests.py @@ -9,12 +9,9 @@ from django.http import HttpResponse, HttpRequest from django.contrib.auth import get_user_model -from .models import DataUseAgreement, DataUseAgreementSign, DataProject +from .models import DataUseAgreement from .views import submit_request -# TODO DELETE THIS -from .views import submit_request2 - TEST_JWT_TOKEN = "WIUHDI&WHQDBKbIYWGD^GUQG^DG&wdydwg^@@Ejdh37364BQWKDBKWDU##B@@9wUDBi&@GiYWBD" TEST_USER_EMAIL = "test-user@example.com" @@ -39,4 +36,3 @@ def test_every_dua_form_file_exists(self): for dua in data_use_agreements: form_path = os.path.join(settings.DATA_USE_AGREEMENT_FORM_DIR, dua.agreement_form_file) self.assertEqual(True, os.path.isfile(form_path), "DUA '" + dua.name + "' agreement_form_file '" + dua.agreement_form_file + "' missing in directory " + settings.DATA_USE_AGREEMENT_FORM_DIR + ".") - From 1e3a013ad29d0898c5f258acdc43052e178be6a7 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 19 Dec 2017 15:48:03 -0500 Subject: [PATCH 055/613] Removed unneeded import --- app/dataprojects/tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/dataprojects/tests.py b/app/dataprojects/tests.py index f7d3a9a2..7110bc29 100644 --- a/app/dataprojects/tests.py +++ b/app/dataprojects/tests.py @@ -10,7 +10,6 @@ from django.contrib.auth import get_user_model from .models import DataUseAgreement -from .views import submit_request TEST_JWT_TOKEN = "WIUHDI&WHQDBKbIYWGD^GUQG^DG&wdydwg^@@Ejdh37364BQWKDBKWDU##B@@9wUDBi&@GiYWBD" TEST_USER_EMAIL = "test-user@example.com" From 43b7d53268d35ea27f8b8603f6fd93b2898f2898 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 5 Jan 2018 12:06:49 -0500 Subject: [PATCH 056/613] TC-65: Moved common UI elements to a base template As we add more views and features to Hypatio, we can share the nav bar and other common UI elements through extendable templates. --- app/templates/base.html | 87 +++++++++ app/templates/dataprojects/list.html | 271 ++++++++++----------------- 2 files changed, 183 insertions(+), 175 deletions(-) create mode 100644 app/templates/base.html diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 00000000..05f01021 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,87 @@ +{% load static %} + + + + + + + + + + + + + {% block scripts %} + {% endblock %} + + + + + + +
    + {% block content %} + {% endblock %} +
    + + \ No newline at end of file diff --git a/app/templates/dataprojects/list.html b/app/templates/dataprojects/list.html index afd06fb3..fdc8253b 100644 --- a/app/templates/dataprojects/list.html +++ b/app/templates/dataprojects/list.html @@ -1,185 +1,112 @@ -{% load static %} +{% extends 'base.html' %} {% load dataprojects_extras %} - - - - - - - - - - - + $( document ).ready(function() { + $('.collapse').on('show.bs.collapse', function() { + var icon = $( this ).closest('.panel-default' ).find( '.step-icon' )[0] - - - + }); + +{% endblock %} -
    +{% block content %}
    + + {% block content %} {% endblock %} + + {# Add a placeholder for the modal contact form #} +
    \ No newline at end of file diff --git a/app/templates/dataprojects/list.html b/app/templates/dataprojects/list.html index fdc8253b..2f70e424 100644 --- a/app/templates/dataprojects/list.html +++ b/app/templates/dataprojects/list.html @@ -2,131 +2,27 @@ {% load dataprojects_extras %} {% block scripts %} - {% endblock %} -{% block content %} - +{% block title %}Data Projects{% endblock %} +{% block subtitle %}Click on a project below to learn more{% endblock %} +{% block content %}
    {% for data_project in data_projects %} {% include "dataprojects/blurb.html" with project_counter=forloop.counter0 %}
    {% endfor %}
    - - {# Add a placeholder for the modal contact form #} - {% endblock %} \ No newline at end of file From 224d9e83d9ceab60f052be90a8b85f36520d1c5c Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 5 Jan 2018 14:25:40 -0500 Subject: [PATCH 058/613] TC-65: moved contact html template to its own templates folder --- app/contact/views.py | 2 +- app/templates/{dataprojects => contact}/contact.html | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/templates/{dataprojects => contact}/contact.html (100%) diff --git a/app/contact/views.py b/app/contact/views.py index e28af647..9153a423 100644 --- a/app/contact/views.py +++ b/app/contact/views.py @@ -81,7 +81,7 @@ def contact_form(request): # Generate and render the form. form = ContactForm(initial=initial) - return render(request, 'dataprojects/contact.html', {'contact_form': form}) + return render(request, 'contact/contact.html', {'contact_form': form}) def email_send(subject=None, recipients=None, message=None, extra=None): """ diff --git a/app/templates/dataprojects/contact.html b/app/templates/contact/contact.html similarity index 100% rename from app/templates/dataprojects/contact.html rename to app/templates/contact/contact.html From c5a3608aaa311d92cbe96ad92378e3bd65c729ca Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 5 Jan 2018 14:34:24 -0500 Subject: [PATCH 059/613] TC-50: Adds profiles app with initial views, urls, forms --- app/hypatio/settings.py | 1 + app/hypatio/urls.py | 3 ++- app/profiles/forms.py | 13 +++++++++ app/profiles/urls.py | 6 +++++ app/profiles/views.py | 41 +++++++++++++++++++++++++++++ app/templates/profiles/profile.html | 10 +++++++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/profiles/forms.py create mode 100644 app/profiles/urls.py create mode 100644 app/profiles/views.py create mode 100644 app/templates/profiles/profile.html diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index f1dc5eee..84720aa0 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -54,6 +54,7 @@ 'bootstrap3', 'contact', 'dataprojects', + 'profiles', 'pyauth0jwt', 'raven.contrib.django.raven_compat', ] diff --git a/app/hypatio/urls.py b/app/hypatio/urls.py index 932d8de6..79cb9f2e 100644 --- a/app/hypatio/urls.py +++ b/app/hypatio/urls.py @@ -5,7 +5,8 @@ urlpatterns = [ url(r'^dataprojects/admin/login/', page_not_found, {'exception': Exception('Admin form login disabled.')}), url(r'^admin/', admin.site.urls), - url(r'^dataprojects/', include('dataprojects.urls')), url(r'^contact/', include('contact.urls')), + url(r'^dataprojects/', include('dataprojects.urls')), + url(r'^profiles/', include('profiles.urls')), url(r'^', include('dataprojects.urls')), ] diff --git a/app/profiles/forms.py b/app/profiles/forms.py new file mode 100644 index 00000000..b0a529e5 --- /dev/null +++ b/app/profiles/forms.py @@ -0,0 +1,13 @@ +from django import forms + +class UserProfileForm(forms.Form): + first_name = forms.CharField(label='First Name', max_length=255, required=True) + last_name = forms.CharField(label='Last Name', max_length=255, required=True) + email = forms.EmailField(label='Email', max_length=255, required=True) + street_address1 = forms.CharField(label="Street Address 1", max_length=255, required=False) + street_address2 = forms.CharField(label="Street Address 2", max_length=255, required=False) + city = forms.CharField(label="City", max_length=255, required=False) + state = forms.CharField(label="State", max_length=255, required=False) + zipcode = forms.CharField(label="Zip", max_length=255, required=False) + phone_number = forms.CharField(label="Phone Number", max_length=255, required=False) + diff --git a/app/profiles/urls.py b/app/profiles/urls.py new file mode 100644 index 00000000..b3799eb3 --- /dev/null +++ b/app/profiles/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from .views import profile + +urlpatterns = ( + url(r'^$', profile), +) diff --git a/app/profiles/views.py b/app/profiles/views.py new file mode 100644 index 00000000..c9275790 --- /dev/null +++ b/app/profiles/views.py @@ -0,0 +1,41 @@ +import json +import logging +import sys +import requests +from datetime import datetime + +from django.conf import settings +from django.contrib.auth import logout +from django.shortcuts import render, redirect +from pyauth0jwt.auth0authenticate import public_user_auth_and_jwt + +from .forms import UserProfileForm + +from django.http import HttpResponse, HttpResponseRedirect +from django.urls import reverse +from django.contrib import messages + +from django.template.loader import render_to_string + +# Get an instance of a logger +logger = logging.getLogger(__name__) + +@public_user_auth_and_jwt +def profile(request, template_name='profiles/profile.html'): + + # Set initial values. + initial = { + 'first_name': 'test', + 'last_name': 'test', + 'email': 'test', + 'street_address1': 'test', + 'street_address2': 'test', + 'city': 'test', + 'state': 'test', + 'zipcode': 'test', + 'phone_number': 'test' + } + + # Generate and render the form. + form = UserProfileForm(initial=initial) + return render(request, template_name, {'user_profile_form': form}) diff --git a/app/templates/profiles/profile.html b/app/templates/profiles/profile.html new file mode 100644 index 00000000..069cfe15 --- /dev/null +++ b/app/templates/profiles/profile.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %}User Profiles{% endblock %} +{% block subtitle %}Review and update your profile{% endblock %} + +{% block content %} + {% csrf_token %} + {% bootstrap_form user_profile_form %} +{% endblock %} \ No newline at end of file From 3e7dc86dfdc30ae9340fa5614d882a60882017e7 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 5 Jan 2018 16:40:13 -0500 Subject: [PATCH 060/613] TC-50: Adds registration info form and pulls data from SciReg into the form --- app/hypatio/settings.py | 2 +- app/hypatio/urls.py | 2 +- app/profile/__init__.py | 0 app/{profiles => profile}/forms.py | 1 - app/{profiles => profile}/urls.py | 0 app/profile/views.py | 58 +++++++++++++++++++ app/profiles/views.py | 41 ------------- app/templates/base.html | 2 +- .../{profiles => profile}/profile.html | 2 +- 9 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 app/profile/__init__.py rename app/{profiles => profile}/forms.py (99%) rename app/{profiles => profile}/urls.py (100%) create mode 100644 app/profile/views.py delete mode 100644 app/profiles/views.py rename app/templates/{profiles => profile}/profile.html (82%) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 84720aa0..af72ea8d 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -54,7 +54,7 @@ 'bootstrap3', 'contact', 'dataprojects', - 'profiles', + 'profile', 'pyauth0jwt', 'raven.contrib.django.raven_compat', ] diff --git a/app/hypatio/urls.py b/app/hypatio/urls.py index 79cb9f2e..b2a17cfd 100644 --- a/app/hypatio/urls.py +++ b/app/hypatio/urls.py @@ -7,6 +7,6 @@ url(r'^admin/', admin.site.urls), url(r'^contact/', include('contact.urls')), url(r'^dataprojects/', include('dataprojects.urls')), - url(r'^profiles/', include('profiles.urls')), + url(r'^profile/', include('profile.urls')), url(r'^', include('dataprojects.urls')), ] diff --git a/app/profile/__init__.py b/app/profile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/profiles/forms.py b/app/profile/forms.py similarity index 99% rename from app/profiles/forms.py rename to app/profile/forms.py index b0a529e5..e968da03 100644 --- a/app/profiles/forms.py +++ b/app/profile/forms.py @@ -10,4 +10,3 @@ class UserProfileForm(forms.Form): state = forms.CharField(label="State", max_length=255, required=False) zipcode = forms.CharField(label="Zip", max_length=255, required=False) phone_number = forms.CharField(label="Phone Number", max_length=255, required=False) - diff --git a/app/profiles/urls.py b/app/profile/urls.py similarity index 100% rename from app/profiles/urls.py rename to app/profile/urls.py diff --git a/app/profile/views.py b/app/profile/views.py new file mode 100644 index 00000000..9b74883e --- /dev/null +++ b/app/profile/views.py @@ -0,0 +1,58 @@ +import json +import logging +import sys +import requests +from datetime import datetime + +from django.conf import settings +from django.contrib.auth import logout +from django.shortcuts import render, redirect +from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_jwt, logout_redirect + +from .forms import UserProfileForm + +from django.http import HttpResponse, HttpResponseRedirect +from django.urls import reverse +from django.contrib import messages + +from django.template.loader import render_to_string + +# Get an instance of a logger +logger = logging.getLogger(__name__) + +@user_auth_and_jwt +def profile(request, template_name='profile/profile.html'): + + user = request.user + user_logged_in = True + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + # If the JWT has expired or the user doesn't have one, force the user to login again + if user_jwt is None or validate_jwt(request) is None: + logout_redirect(request) + + # The JWT token that will get passed in API calls + jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + # Query SciReg to get the user's information + registration_url = settings.SCIREG_SERVER_URL + '/api/register' + registration_info = requests.get(registration_url, headers=jwt_headers, verify=False).json()["results"] + + # Populate the form with the returned registration info + form_values = { + 'first_name': registration_info[0]['first_name'], + 'last_name': registration_info[0]['last_name'], + 'email': registration_info[0]['email'], + 'street_address1': registration_info[0]['street_address1'], + 'street_address2': registration_info[0]['street_address2'], + 'city': registration_info[0]['city'], + 'state': registration_info[0]['state'], + 'zipcode': registration_info[0]['zipcode'], + 'phone_number': registration_info[0]['phone_number'] + } + + # Generate and render the form. + form = UserProfileForm(initial=form_values) + return render(request, template_name, {'user_profile_form': form, + 'user': user, + 'user_logged_in': user_logged_in}) diff --git a/app/profiles/views.py b/app/profiles/views.py deleted file mode 100644 index c9275790..00000000 --- a/app/profiles/views.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -import logging -import sys -import requests -from datetime import datetime - -from django.conf import settings -from django.contrib.auth import logout -from django.shortcuts import render, redirect -from pyauth0jwt.auth0authenticate import public_user_auth_and_jwt - -from .forms import UserProfileForm - -from django.http import HttpResponse, HttpResponseRedirect -from django.urls import reverse -from django.contrib import messages - -from django.template.loader import render_to_string - -# Get an instance of a logger -logger = logging.getLogger(__name__) - -@public_user_auth_and_jwt -def profile(request, template_name='profiles/profile.html'): - - # Set initial values. - initial = { - 'first_name': 'test', - 'last_name': 'test', - 'email': 'test', - 'street_address1': 'test', - 'street_address2': 'test', - 'city': 'test', - 'state': 'test', - 'zipcode': 'test', - 'phone_number': 'test' - } - - # Generate and render the form. - form = UserProfileForm(initial=initial) - return render(request, template_name, {'user_profile_form': form}) diff --git a/app/templates/base.html b/app/templates/base.html index ed6827b6..fdc6afec 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -152,7 +152,7 @@ diff --git a/app/templates/profiles/profile.html b/app/templates/profile/profile.html similarity index 82% rename from app/templates/profiles/profile.html rename to app/templates/profile/profile.html index 069cfe15..bf09f09a 100644 --- a/app/templates/profiles/profile.html +++ b/app/templates/profile/profile.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load bootstrap3 %} -{% block title %}User Profiles{% endblock %} +{% block title %}User Profile{% endblock %} {% block subtitle %}Review and update your profile{% endblock %} {% block content %} From c8531e0eacc34c890a3b7700e83fd21775358caf Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 10 Jan 2018 09:33:51 -0500 Subject: [PATCH 061/613] TC-50: Adds registration form with reading from and posting to SciReg Also updated to Bootstrap v4 --- app/hypatio/settings.py | 2 +- app/profile/forms.py | 7 +++ app/profile/views.py | 69 ++++++++++++++++++++---------- app/requirements.txt | 2 +- app/templates/contact/contact.html | 2 +- app/templates/profile/profile.html | 33 ++++++++++++-- 6 files changed, 86 insertions(+), 29 deletions(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index af72ea8d..45dafde1 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -51,7 +51,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'jquery', - 'bootstrap3', + 'bootstrap4', 'contact', 'dataprojects', 'profile', diff --git a/app/profile/forms.py b/app/profile/forms.py index e968da03..13203dd8 100644 --- a/app/profile/forms.py +++ b/app/profile/forms.py @@ -1,6 +1,12 @@ from django import forms class UserProfileForm(forms.Form): + + # These fields need to be included in order to match serialization on SciReg side, but we do not want the users touching them + id = forms.CharField(label='ID', max_length=255, required=True, widget=forms.TextInput(attrs={'readonly':'readonly', 'type':'hidden'})) + twitter_handle = forms.CharField(label="Twitter Handle", max_length=255, required=False, widget=forms.TextInput(attrs={'readonly':'readonly', 'type':'hidden'})) + email_confirmed = forms.CharField(label="Email confirmed", max_length=255, required=False, widget=forms.TextInput(attrs={'readonly':'readonly', 'type':'hidden'})) + first_name = forms.CharField(label='First Name', max_length=255, required=True) last_name = forms.CharField(label='Last Name', max_length=255, required=True) email = forms.EmailField(label='Email', max_length=255, required=True) @@ -10,3 +16,4 @@ class UserProfileForm(forms.Form): state = forms.CharField(label="State", max_length=255, required=False) zipcode = forms.CharField(label="Zip", max_length=255, required=False) phone_number = forms.CharField(label="Phone Number", max_length=255, required=False) + \ No newline at end of file diff --git a/app/profile/views.py b/app/profile/views.py index 9b74883e..3e5d5e29 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -34,25 +34,50 @@ def profile(request, template_name='profile/profile.html'): # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} - # Query SciReg to get the user's information - registration_url = settings.SCIREG_SERVER_URL + '/api/register' - registration_info = requests.get(registration_url, headers=jwt_headers, verify=False).json()["results"] - - # Populate the form with the returned registration info - form_values = { - 'first_name': registration_info[0]['first_name'], - 'last_name': registration_info[0]['last_name'], - 'email': registration_info[0]['email'], - 'street_address1': registration_info[0]['street_address1'], - 'street_address2': registration_info[0]['street_address2'], - 'city': registration_info[0]['city'], - 'state': registration_info[0]['state'], - 'zipcode': registration_info[0]['zipcode'], - 'phone_number': registration_info[0]['phone_number'] - } - - # Generate and render the form. - form = UserProfileForm(initial=form_values) - return render(request, template_name, {'user_profile_form': form, - 'user': user, - 'user_logged_in': user_logged_in}) + # If there was a POST request, a form was submitted + if request.method == 'POST': + + # Process the form + form = UserProfileForm(request.POST) + if form.is_valid(): + + # The SciReg API endpoint + registration_url = settings.SCIREG_SERVER_URL + '/api/register/' + form.cleaned_data['id'] + '/' + + logger.debug('[HYPATIO][DEBUG] Profile form fields submitted: ' + json.dumps(form.cleaned_data)) + + # Send the data to SciReg + requests.put(registration_url, headers=jwt_headers, data=json.dumps(form.cleaned_data), verify=False) + + # Generate and render the form. + return render(request, template_name, {'form': form, + 'user': user, + 'user_logged_in': user_logged_in}) + else: + logger.debug('[HYPATIO][DEBUG] Profile form errors: ' + form.errors.as_json()) + + # TODO send this back to profile to display errors + return HttpResponse(status=500) + + else: + + # The SciReg API endpoint + registration_url = settings.SCIREG_SERVER_URL + '/api/register/' + + # Query SciReg to get the user's information + registration_info = requests.get(registration_url, headers=jwt_headers, verify=False).json() + + if registration_info is not None: + registration_info = registration_info["results"] + else: + # TODO user does not have a registration in scireg, what should we do? + pass + + logger.debug('[HYPATIO][DEBUG] Registration info ' + json.dumps(registration_info)) + + # Generate and render the form. + form = UserProfileForm(initial=registration_info[0]) + return render(request, template_name, {'form': form, + 'user': user, + 'user_logged_in': user_logged_in}) + diff --git a/app/requirements.txt b/app/requirements.txt index 5fcb0900..f200c394 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,5 @@ Django==1.10.1 -django-bootstrap3==9.0.0 +django-bootstrap4==0.0.5 django-jquery==3.1.0 django-stronghold==0.2.8 djangorestframework==3.5.3 diff --git a/app/templates/contact/contact.html b/app/templates/contact/contact.html index 201dfe19..eb8246b5 100644 --- a/app/templates/contact/contact.html +++ b/app/templates/contact/contact.html @@ -1,4 +1,4 @@ -{% load bootstrap3 %} +{% load bootstrap4 %}
  • diff --git a/app/templates/datachallenges/blurb.html b/app/templates/datachallenges/blurb.html new file mode 100644 index 00000000..b2a8bd69 --- /dev/null +++ b/app/templates/datachallenges/blurb.html @@ -0,0 +1,20 @@ +
    + +
    +
    + {% autoescape off %} + {{ data_challenge.description }} + {% endautoescape %} +
    +
    +
    diff --git a/app/templates/datachallenges/list.html b/app/templates/datachallenges/list.html new file mode 100644 index 00000000..53e8a990 --- /dev/null +++ b/app/templates/datachallenges/list.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load dataprojects_extras %} + +{% block scripts %} +{% endblock %} + +{% block title %}Data Challenges{% endblock %} +{% block subtitle %}Click on a challenge below to learn more{% endblock %} + +{% block content %} +
    + {% for data_challenge in data_challenges %} + {% include "datachallenges/blurb.html" with challenge_counter=forloop.counter0 %} +
    + {% endfor %} +
    +{% endblock %} \ No newline at end of file From c3dd0e8ead3d48aa0fbb5c17a664b3fcde3c0ba7 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 11 Jan 2018 14:07:21 -0500 Subject: [PATCH 064/613] TC-72: Added checks to see that SciAuthZ returns json before attempting to pull specific element --- app/dataprojects/views.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/dataprojects/views.py b/app/dataprojects/views.py index e4a2a23d..8611a723 100644 --- a/app/dataprojects/views.py +++ b/app/dataprojects/views.py @@ -92,21 +92,29 @@ def list_data_projects(request, template_name='dataprojects/list.html'): # Get all of the user's VIEW permissions permissions_url = settings.PERMISSION_SERVER - user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json()["results"] + user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() + logger.debug('[HYPATIO][DEBUG] User Permissions: ' + json.dumps(user_permissions)) - for user_permission in user_permissions: - if user_permission['permission'] == 'VIEW': - projects_with_view_permissions.append(user_permission['item']) + if user_permissions is not None and 'results' in user_permissions: + user_permissions = user_permissions["results"] + + for user_permission in user_permissions: + if user_permission['permission'] == 'VIEW': + projects_with_view_permissions.append(user_permission['item']) # Get all of the user's permission requests access_requests_url = settings.GET_ACCESS_REQUESTS - user_access_requests = requests.get(access_requests_url, headers=jwt_headers, verify=False).json()["results"] + user_access_requests = requests.get(access_requests_url, headers=jwt_headers, verify=False).json() + logger.debug('[HYPATIO][DEBUG] User Permission Requests: ' + json.dumps(user_access_requests)) + + if user_access_requests is not None and 'results' in user_access_requests: + user_access_requests = user_access_requests["results"] - for access_request in user_access_requests: - projects_with_access_requests[access_request['item']] = { - 'date_requested': access_request['date_requested'], - 'request_granted': access_request['request_granted'], - 'date_request_granted': access_request['date_request_granted']} + for access_request in user_access_requests: + projects_with_access_requests[access_request['item']] = { + 'date_requested': access_request['date_requested'], + 'request_granted': access_request['request_granted'], + 'date_request_granted': access_request['date_request_granted']} # Build the dictionary with all project and permission information needed for project in all_data_projects: From 550910464e0ad1cef0b9e86c8f24a98fc59dc154 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 11 Jan 2018 14:39:01 -0500 Subject: [PATCH 065/613] TC-40: Settings file needs datachallenges as an app --- app/hypatio/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 45dafde1..39f885af 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -53,6 +53,7 @@ 'jquery', 'bootstrap4', 'contact', + 'datachallenges', 'dataprojects', 'profile', 'pyauth0jwt', From 2ffe3dc18b88e010a0014605df3841a7e496b8d0 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 12 Jan 2018 09:47:43 -0500 Subject: [PATCH 066/613] TC-75: replaces sqlite with mysql database from RDS instance --- app/hypatio/settings.py | 9 ++++++--- gunicorn-nginx-entry.sh | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 45dafde1..2ff4eee5 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -96,12 +96,15 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'hypatio', + 'USER': os.environ.get("MYSQL_USERNAME"), + 'PASSWORD': os.environ.get("MYSQL_PASSWORD"), + 'HOST': os.environ.get("MYSQL_HOST"), + 'PORT': os.environ.get("MYSQL_PORT"), } } - # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index ff16c20a..0b5b9037 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -8,6 +8,11 @@ AUTH0_SUCCESS_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_success_ AUTH0_LOGOUT_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_logout_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') COOKIE_DOMAIN=$(aws ssm get-parameters --names $PS_PATH.cookie_domain --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +MYSQL_USERNAME_VAULT=$(aws ssm get-parameters --names $PS_PATH.mysql_username --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +MYSQL_PASSWORD_VAULT=$(aws ssm get-parameters --names $PS_PATH.mysql_pw --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +MYSQL_HOST_VAULT=$(aws ssm get-parameters --names $PS_PATH.mysql_host --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +MYSQL_PORT_VAULT=$(aws ssm get-parameters --names $PS_PATH.mysql_port --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') + EMAIL_HOST=$(aws ssm get-parameters --names $PS_PATH.email_host --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') EMAIL_HOST_USER=$(aws ssm get-parameters --names $PS_PATH.email_host_user --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') EMAIL_HOST_PASSWORD=$(aws ssm get-parameters --names $PS_PATH.email_host_password --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') @@ -21,6 +26,11 @@ export AUTH0_SECRET=$AUTH0_SECRET_VAULT export AUTH0_SUCCESS_URL=$AUTH0_SUCCESS_URL_VAULT export AUTH0_LOGOUT_URL=$AUTH0_LOGOUT_URL_VAULT +export MYSQL_USERNAME=$MYSQL_USERNAME_VAULT +export MYSQL_PASSWORD=$MYSQL_PASSWORD_VAULT +export MYSQL_HOST=$MYSQL_HOST_VAULT +export MYSQL_PORT=$MYSQL_PORT_VAULT + export EMAIL_HOST export EMAIL_HOST_USER export EMAIL_HOST_PASSWORD From c58e8cea82b76f2e40e01a05a72220b6e96aa081 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 13 Jan 2018 12:59:33 -0500 Subject: [PATCH 067/613] TC-51 - Added email verification to the profile page --- app/hypatio/scireg_services.py | 31 ++++++++++++++ app/hypatio/settings.py | 5 +++ app/profile/urls.py | 3 +- app/profile/views.py | 69 +++++++++++++++++++++++++----- app/static/animate.css | 11 +++++ app/templates/base.html | 22 ++++++---- app/templates/profile/profile.html | 61 ++++++++++++++++++++++++++ gunicorn-nginx-entry.sh | 5 ++- 8 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 app/static/animate.css diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index 3631d557..9b31c7e7 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -1,6 +1,37 @@ import requests +import json from django.conf import settings +import logging +logger = logging.getLogger(__name__) + def build_headers_with_jwt(user_jwt): return {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + +def send_confirmation_email(user_jwt): + send_confirm_email_url = settings.SCIREG_SERVER_URL + '/api/register/send_confirmation_email/' + + logger.debug("[P2M2][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) + + email_confirm_data = { + 'success_url': settings.EMAIL_CONFIRM_SUCCESS_URL + } + + requests.post(send_confirm_email_url, headers=build_headers_with_jwt(user_jwt), data=json.dumps(email_confirm_data)) + + +def check_email_confirmation(user_jwt): + check_email_confirm_url = settings.SCIREG_SERVER_URL + '/api/register/' + + response = requests.get(check_email_confirm_url, headers=build_headers_with_jwt(user_jwt)) + + try: + email_status = response.json()['results'][0]['email_confirmed'] + except KeyError: + email_status = None + except IndexError: + email_status = None + + return email_status \ No newline at end of file diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index e221748b..dd4d763a 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -147,6 +147,11 @@ CONTACT_FORM_RECIPIENTS="dbmi_tech_core@hms.harvard.edu" DEFAULT_FROM_EMAIL="dbmi_tech_core@hms.harvard.edu" +RECAPTCHA_KEY = os.environ.get('RECAPTCHA_KEY') +RECAPTCHA_CLIENT_ID = os.environ.get('RECAPTCHA_CLIENT_ID') + +EMAIL_CONFIRM_SUCCESS_URL = os.environ.get('EMAIL_CONFIRM_SUCCESS_URL') + # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ diff --git a/app/profile/urls.py b/app/profile/urls.py index b3799eb3..608d7a13 100644 --- a/app/profile/urls.py +++ b/app/profile/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import url -from .views import profile +from .views import profile, send_confirmation_email_view urlpatterns = ( url(r'^$', profile), + url(r'^send_confirmation_email/$', send_confirmation_email_view) ) diff --git a/app/profile/views.py b/app/profile/views.py index e9dda0fb..2b461db9 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -1,21 +1,15 @@ import json import logging -import sys import requests -from datetime import datetime from django.conf import settings -from django.contrib.auth import logout -from django.shortcuts import render, redirect +from django.shortcuts import render from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_jwt, logout_redirect - from .forms import RegistrationForm +from django.http import HttpResponse -from django.http import HttpResponse, HttpResponseRedirect -from django.urls import reverse -from django.contrib import messages +from hypatio import scireg_services -from django.template.loader import render_to_string # Get an instance of a logger logger = logging.getLogger(__name__) @@ -83,4 +77,59 @@ def profile(request, template_name='profile/profile.html'): return render(request, template_name, {'form': form, 'user': user, 'new_user': new_user, - 'user_logged_in': user_logged_in}) + 'user_logged_in': user_logged_in, + 'recaptcha_client_id': settings.RECAPTCHA_CLIENT_ID}) + + +def get_client_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + +def recaptcha_check(request): + """ + Send a query over to google's servers with the result of the Captcha to see whether it's valid. + :param request: + :return: + """ + response = {} + + captcha_rs = request.POST.get('g-recaptcha-response') + + url = "https://www.google.com/recaptcha/api/siteverify" + + params = { + 'secret': settings.RECAPTCHA_KEY, + 'response': captcha_rs, + 'remoteip': get_client_ip(request) + } + + logger.debug("[P2M2][DEBUG][recaptcha_check] Sending Captcha results to google - " + str(request.user.id)) + + verify_rs = requests.get(url, params=params, verify=True) + verify_rs = verify_rs.json() + response["status"] = verify_rs.get("success", False) + response['message'] = verify_rs.get('error-codes', None) or "Unspecified error." + + return response + +@user_auth_and_jwt +def send_confirmation_email_view(request): + logger.debug("[P2M2][DEBUG][send_confirmation_email_view] Sending user verification e-mail - " + str(request.user.id)) + + if request.method == 'POST': + + # Need to verify the Google Recaptcha before sending e-mail. + recaptcha_response = recaptcha_check(request) + + if recaptcha_response["status"]: + scireg_services.send_confirmation_email(request.COOKIES.get("DBMI_JWT", None)) + return HttpResponse("SENT") + else: + return HttpResponse("FAILED_RECAPTCHA") + else: + return HttpResponse("INVALID_POST") \ No newline at end of file diff --git a/app/static/animate.css b/app/static/animate.css new file mode 100644 index 00000000..e7dd6550 --- /dev/null +++ b/app/static/animate.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +/*! + * animate.css -http://daneden.me/animate + * Version - 3.5.2 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2017 Daniel Eden + */ + +.animated{animation-duration:1s;animation-fill-mode:both}.animated.infinite{animation-iteration-count:infinite}.animated.hinge{animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{animation-duration:.75s}@keyframes bounce{0%,20%,53%,80%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1);transform:translateZ(0)}40%,43%{animation-timing-function:cubic-bezier(.755,.05,.855,.06);transform:translate3d(0,-30px,0)}70%{animation-timing-function:cubic-bezier(.755,.05,.855,.06);transform:translate3d(0,-15px,0)}90%{transform:translate3d(0,-4px,0)}}.bounce{animation-name:bounce;transform-origin:center bottom}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{animation-name:flash}@keyframes pulse{0%{transform:scaleX(1)}50%{transform:scale3d(1.05,1.05,1.05)}to{transform:scaleX(1)}}.pulse{animation-name:pulse}@keyframes rubberBand{0%{transform:scaleX(1)}30%{transform:scale3d(1.25,.75,1)}40%{transform:scale3d(.75,1.25,1)}50%{transform:scale3d(1.15,.85,1)}65%{transform:scale3d(.95,1.05,1)}75%{transform:scale3d(1.05,.95,1)}to{transform:scaleX(1)}}.rubberBand{animation-name:rubberBand}@keyframes shake{0%,to{transform:translateZ(0)}10%,30%,50%,70%,90%{transform:translate3d(-10px,0,0)}20%,40%,60%,80%{transform:translate3d(10px,0,0)}}.shake{animation-name:shake}@keyframes headShake{0%{transform:translateX(0)}6.5%{transform:translateX(-6px) rotateY(-9deg)}18.5%{transform:translateX(5px) rotateY(7deg)}31.5%{transform:translateX(-3px) rotateY(-5deg)}43.5%{transform:translateX(2px) rotateY(3deg)}50%{transform:translateX(0)}}.headShake{animation-timing-function:ease-in-out;animation-name:headShake}@keyframes swing{20%{transform:rotate(15deg)}40%{transform:rotate(-10deg)}60%{transform:rotate(5deg)}80%{transform:rotate(-5deg)}to{transform:rotate(0deg)}}.swing{transform-origin:top center;animation-name:swing}@keyframes tada{0%{transform:scaleX(1)}10%,20%{transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{transform:scaleX(1)}}.tada{animation-name:tada}@keyframes wobble{0%{transform:none}15%{transform:translate3d(-25%,0,0) rotate(-5deg)}30%{transform:translate3d(20%,0,0) rotate(3deg)}45%{transform:translate3d(-15%,0,0) rotate(-3deg)}60%{transform:translate3d(10%,0,0) rotate(2deg)}75%{transform:translate3d(-5%,0,0) rotate(-1deg)}to{transform:none}}.wobble{animation-name:wobble}@keyframes jello{0%,11.1%,to{transform:none}22.2%{transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{transform:skewX(6.25deg) skewY(6.25deg)}44.4%{transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{transform:skewX(.390625deg) skewY(.390625deg)}88.8%{transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{animation-name:jello;transform-origin:center}@keyframes bounceIn{0%,20%,40%,60%,80%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:scale3d(.3,.3,.3)}20%{transform:scale3d(1.1,1.1,1.1)}40%{transform:scale3d(.9,.9,.9)}60%{opacity:1;transform:scale3d(1.03,1.03,1.03)}80%{transform:scale3d(.97,.97,.97)}to{opacity:1;transform:scaleX(1)}}.bounceIn{animation-name:bounceIn}@keyframes bounceInDown{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,-3000px,0)}60%{opacity:1;transform:translate3d(0,25px,0)}75%{transform:translate3d(0,-10px,0)}90%{transform:translate3d(0,5px,0)}to{transform:none}}.bounceInDown{animation-name:bounceInDown}@keyframes bounceInLeft{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(-3000px,0,0)}60%{opacity:1;transform:translate3d(25px,0,0)}75%{transform:translate3d(-10px,0,0)}90%{transform:translate3d(5px,0,0)}to{transform:none}}.bounceInLeft{animation-name:bounceInLeft}@keyframes bounceInRight{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(3000px,0,0)}60%{opacity:1;transform:translate3d(-25px,0,0)}75%{transform:translate3d(10px,0,0)}90%{transform:translate3d(-5px,0,0)}to{transform:none}}.bounceInRight{animation-name:bounceInRight}@keyframes bounceInUp{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,3000px,0)}60%{opacity:1;transform:translate3d(0,-20px,0)}75%{transform:translate3d(0,10px,0)}90%{transform:translate3d(0,-5px,0)}to{transform:translateZ(0)}}.bounceInUp{animation-name:bounceInUp}@keyframes bounceOut{20%{transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;transform:scale3d(1.1,1.1,1.1)}to{opacity:0;transform:scale3d(.3,.3,.3)}}.bounceOut{animation-name:bounceOut}@keyframes bounceOutDown{20%{transform:translate3d(0,10px,0)}40%,45%{opacity:1;transform:translate3d(0,-20px,0)}to{opacity:0;transform:translate3d(0,2000px,0)}}.bounceOutDown{animation-name:bounceOutDown}@keyframes bounceOutLeft{20%{opacity:1;transform:translate3d(20px,0,0)}to{opacity:0;transform:translate3d(-2000px,0,0)}}.bounceOutLeft{animation-name:bounceOutLeft}@keyframes bounceOutRight{20%{opacity:1;transform:translate3d(-20px,0,0)}to{opacity:0;transform:translate3d(2000px,0,0)}}.bounceOutRight{animation-name:bounceOutRight}@keyframes bounceOutUp{20%{transform:translate3d(0,-10px,0)}40%,45%{opacity:1;transform:translate3d(0,20px,0)}to{opacity:0;transform:translate3d(0,-2000px,0)}}.bounceOutUp{animation-name:bounceOutUp}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{animation-name:fadeIn}@keyframes fadeInDown{0%{opacity:0;transform:translate3d(0,-100%,0)}to{opacity:1;transform:none}}.fadeInDown{animation-name:fadeInDown}@keyframes fadeInDownBig{0%{opacity:0;transform:translate3d(0,-2000px,0)}to{opacity:1;transform:none}}.fadeInDownBig{animation-name:fadeInDownBig}@keyframes fadeInLeft{0%{opacity:0;transform:translate3d(-100%,0,0)}to{opacity:1;transform:none}}.fadeInLeft{animation-name:fadeInLeft}@keyframes fadeInLeftBig{0%{opacity:0;transform:translate3d(-2000px,0,0)}to{opacity:1;transform:none}}.fadeInLeftBig{animation-name:fadeInLeftBig}@keyframes fadeInRight{0%{opacity:0;transform:translate3d(100%,0,0)}to{opacity:1;transform:none}}.fadeInRight{animation-name:fadeInRight}@keyframes fadeInRightBig{0%{opacity:0;transform:translate3d(2000px,0,0)}to{opacity:1;transform:none}}.fadeInRightBig{animation-name:fadeInRightBig}@keyframes fadeInUp{0%{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:none}}.fadeInUp{animation-name:fadeInUp}@keyframes fadeInUpBig{0%{opacity:0;transform:translate3d(0,2000px,0)}to{opacity:1;transform:none}}.fadeInUpBig{animation-name:fadeInUpBig}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{animation-name:fadeOut}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;transform:translate3d(0,100%,0)}}.fadeOutDown{animation-name:fadeOutDown}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;transform:translate3d(0,2000px,0)}}.fadeOutDownBig{animation-name:fadeOutDownBig}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;transform:translate3d(-100%,0,0)}}.fadeOutLeft{animation-name:fadeOutLeft}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{animation-name:fadeOutLeftBig}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;transform:translate3d(100%,0,0)}}.fadeOutRight{animation-name:fadeOutRight}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;transform:translate3d(2000px,0,0)}}.fadeOutRightBig{animation-name:fadeOutRightBig}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;transform:translate3d(0,-100%,0)}}.fadeOutUp{animation-name:fadeOutUp}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{animation-name:fadeOutUpBig}@keyframes flip{0%{transform:perspective(400px) rotateY(-1turn);animation-timing-function:ease-out}40%{transform:perspective(400px) translateZ(150px) rotateY(-190deg);animation-timing-function:ease-out}50%{transform:perspective(400px) translateZ(150px) rotateY(-170deg);animation-timing-function:ease-in}80%{transform:perspective(400px) scale3d(.95,.95,.95);animation-timing-function:ease-in}to{transform:perspective(400px);animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;animation-name:flip}@keyframes flipInX{0%{transform:perspective(400px) rotateX(90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotateX(-20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotateX(10deg);opacity:1}80%{transform:perspective(400px) rotateX(-5deg)}to{transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;animation-name:flipInX}@keyframes flipInY{0%{transform:perspective(400px) rotateY(90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotateY(-20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotateY(10deg);opacity:1}80%{transform:perspective(400px) rotateY(-5deg)}to{transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;animation-name:flipInY}@keyframes flipOutX{0%{transform:perspective(400px)}30%{transform:perspective(400px) rotateX(-20deg);opacity:1}to{transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@keyframes flipOutY{0%{transform:perspective(400px)}30%{transform:perspective(400px) rotateY(-15deg);opacity:1}to{transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;animation-name:flipOutY}@keyframes lightSpeedIn{0%{transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{transform:skewX(20deg);opacity:1}80%{transform:skewX(-5deg);opacity:1}to{transform:none;opacity:1}}.lightSpeedIn{animation-name:lightSpeedIn;animation-timing-function:ease-out}@keyframes lightSpeedOut{0%{opacity:1}to{transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{animation-name:lightSpeedOut;animation-timing-function:ease-in}@keyframes rotateIn{0%{transform-origin:center;transform:rotate(-200deg);opacity:0}to{transform-origin:center;transform:none;opacity:1}}.rotateIn{animation-name:rotateIn}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;transform:rotate(-45deg);opacity:0}to{transform-origin:left bottom;transform:none;opacity:1}}.rotateInDownLeft{animation-name:rotateInDownLeft}@keyframes rotateInDownRight{0%{transform-origin:right bottom;transform:rotate(45deg);opacity:0}to{transform-origin:right bottom;transform:none;opacity:1}}.rotateInDownRight{animation-name:rotateInDownRight}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;transform:rotate(45deg);opacity:0}to{transform-origin:left bottom;transform:none;opacity:1}}.rotateInUpLeft{animation-name:rotateInUpLeft}@keyframes rotateInUpRight{0%{transform-origin:right bottom;transform:rotate(-90deg);opacity:0}to{transform-origin:right bottom;transform:none;opacity:1}}.rotateInUpRight{animation-name:rotateInUpRight}@keyframes rotateOut{0%{transform-origin:center;opacity:1}to{transform-origin:center;transform:rotate(200deg);opacity:0}}.rotateOut{animation-name:rotateOut}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}to{transform-origin:left bottom;transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{animation-name:rotateOutDownLeft}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}to{transform-origin:right bottom;transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{animation-name:rotateOutDownRight}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}to{transform-origin:left bottom;transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{animation-name:rotateOutUpLeft}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}to{transform-origin:right bottom;transform:rotate(90deg);opacity:0}}.rotateOutUpRight{animation-name:rotateOutUpRight}@keyframes hinge{0%{transform-origin:top left;animation-timing-function:ease-in-out}20%,60%{transform:rotate(80deg);transform-origin:top left;animation-timing-function:ease-in-out}40%,80%{transform:rotate(60deg);transform-origin:top left;animation-timing-function:ease-in-out;opacity:1}to{transform:translate3d(0,700px,0);opacity:0}}.hinge{animation-name:hinge}@keyframes jackInTheBox{0%{opacity:0;transform:scale(.1) rotate(30deg);transform-origin:center bottom}50%{transform:rotate(-10deg)}70%{transform:rotate(3deg)}to{opacity:1;transform:scale(1)}}.jackInTheBox{animation-name:jackInTheBox}@keyframes rollIn{0%{opacity:0;transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;transform:none}}.rollIn{animation-name:rollIn}@keyframes rollOut{0%{opacity:1}to{opacity:0;transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{animation-name:rollOut}@keyframes zoomIn{0%{opacity:0;transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{animation-name:zoomIn}@keyframes zoomInDown{0%{opacity:0;transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(0,60px,0);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{animation-name:zoomInDown}@keyframes zoomInLeft{0%{opacity:0;transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(10px,0,0);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{animation-name:zoomInLeft}@keyframes zoomInRight{0%{opacity:0;transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{animation-name:zoomInRight}@keyframes zoomInUp{0%{opacity:0;transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{animation-name:zoomInUp}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;transform:scale3d(.3,.3,.3)}to{opacity:0}}.zoomOut{animation-name:zoomOut}@keyframes zoomOutDown{40%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform-origin:center bottom;animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{animation-name:zoomOutDown}@keyframes zoomOutLeft{40%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;transform:scale(.1) translate3d(-2000px,0,0);transform-origin:left center}}.zoomOutLeft{animation-name:zoomOutLeft}@keyframes zoomOutRight{40%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;transform:scale(.1) translate3d(2000px,0,0);transform-origin:right center}}.zoomOutRight{animation-name:zoomOutRight}@keyframes zoomOutUp{40%{opacity:1;transform:scale3d(.475,.475,.475) translate3d(0,60px,0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform-origin:center bottom;animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{animation-name:zoomOutUp}@keyframes slideInDown{0%{transform:translate3d(0,-100%,0);visibility:visible}to{transform:translateZ(0)}}.slideInDown{animation-name:slideInDown}@keyframes slideInLeft{0%{transform:translate3d(-100%,0,0);visibility:visible}to{transform:translateZ(0)}}.slideInLeft{animation-name:slideInLeft}@keyframes slideInRight{0%{transform:translate3d(100%,0,0);visibility:visible}to{transform:translateZ(0)}}.slideInRight{animation-name:slideInRight}@keyframes slideInUp{0%{transform:translate3d(0,100%,0);visibility:visible}to{transform:translateZ(0)}}.slideInUp{animation-name:slideInUp}@keyframes slideOutDown{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(0,100%,0)}}.slideOutDown{animation-name:slideOutDown}@keyframes slideOutLeft{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(-100%,0,0)}}.slideOutLeft{animation-name:slideOutLeft}@keyframes slideOutRight{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(100%,0,0)}}.slideOutRight{animation-name:slideOutRight}@keyframes slideOutUp{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(0,-100%,0)}}.slideOutUp{animation-name:slideOutUp} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index b1229a20..729d54d7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,16 +4,20 @@ - + - + + + + - + + {% block scripts %} @@ -195,4 +199,6 @@

    {% block title %}{% endblock %}

    + {# Allow for some javascript to be added per page #} + {% block javascript %}{% endblock %} \ No newline at end of file diff --git a/app/templates/profile/profile.html b/app/templates/profile/profile.html index 0a8ba9ca..1b7bd74d 100644 --- a/app/templates/profile/profile.html +++ b/app/templates/profile/profile.html @@ -1,6 +1,8 @@ {% extends 'base.html' %} {% load bootstrap4 %} +{% block page_title %}Profile{% endblock %} + {% block title %} {% if new_user %} New User @@ -23,6 +25,32 @@ Welcome to Hypatio! Please complete your registration form below to get started. {% endif %} + {% if not form.email_confirmed.value %} + + {% endif %}
    {% csrf_token %} @@ -45,4 +73,37 @@ +{% endblock %} + +{% block javascript %} + {% endblock %} \ No newline at end of file diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index 0b5b9037..7957cf33 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -44,6 +44,10 @@ export ACCOUNT_SERVER_URL export SCIREG_SERVER_URL export AUTHZ_BASE +export RECAPTCHA_KEY=$(aws ssm get-parameters --names $PS_PATH.recaptcha_key --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +export RECAPTCHA_CLIENT_ID=$(aws ssm get-parameters --names $PS_PATH.recaptcha_client_id --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +export EMAIL_CONFIRM_SUCCESS_URL=$(aws ssm get-parameters --names $PS_PATH.email_confirm_success_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') + SSL_KEY=$(aws ssm get-parameters --names $PS_PATH.ssl_key --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') SSL_CERT_CHAIN1=$(aws ssm get-parameters --names $PS_PATH.ssl_cert_chain1 --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') SSL_CERT_CHAIN2=$(aws ssm get-parameters --names $PS_PATH.ssl_cert_chain2 --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') @@ -57,7 +61,6 @@ echo $SSL_CERT_CHAIN | base64 -d >> /etc/nginx/ssl/server.crt cd /app/ python manage.py migrate -python manage.py loaddata dataprojects if [ ! -d static ]; then mkdir static fi From 754311fc288d6d034e9c36d77c031eea64835cbc Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 13 Jan 2018 14:52:20 -0500 Subject: [PATCH 068/613] TC-51 - Shows email as verified when it is --- app/profile/forms.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/profile/forms.py b/app/profile/forms.py index 18d0f348..eb8a2a7f 100644 --- a/app/profile/forms.py +++ b/app/profile/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.utils.safestring import mark_safe class RegistrationForm(forms.Form): @@ -31,3 +32,10 @@ def __init__(self, *args, **kwargs): if not new_registration: self.fields['first_name'].widget = forms.TextInput(attrs={'readonly':'readonly'}) self.fields['last_name'].widget = forms.TextInput(attrs={'readonly':'readonly'}) + + # Check for email verified. + if self.initial.get('email_confirmed', False): + + # Mark it as such. + self.fields['email'].help_text = mark_safe('
    Verified!
    ') + From 88fa95b6592f05c178f9cb7babdcb6966fb3a0bd Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 13 Jan 2018 14:53:10 -0500 Subject: [PATCH 069/613] TC-51 - Setup to display messages and returning task messages from external services --- app/profile/views.py | 28 +++++++++++++++++++++++++- app/templates/messages.html | 32 ++++++++++++++++++++++++++++++ app/templates/profile/profile.html | 4 ++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/templates/messages.html diff --git a/app/profile/views.py b/app/profile/views.py index 2b461db9..53fa827b 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -2,6 +2,7 @@ import logging import requests +from django.contrib import messages from django.conf import settings from django.shortcuts import render from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_jwt, logout_redirect @@ -67,12 +68,16 @@ def profile(request, template_name='profile/profile.html'): if registration_info['count'] != 0: registration_info = registration_info["results"][0] form = RegistrationForm(initial=registration_info) + new_user = False else: # User does not have a registration in scireg, present them with a blank form to complete and prepopulate the email form = RegistrationForm(initial={'email': user.email}, new_registration=True) new_user = True + # Check for a returning task and set messages accordingly + get_task_context_data(request) + # Generate and render the form. return render(request, template_name, {'form': form, 'user': user, @@ -132,4 +137,25 @@ def send_confirmation_email_view(request): else: return HttpResponse("FAILED_RECAPTCHA") else: - return HttpResponse("INVALID_POST") \ No newline at end of file + return HttpResponse("INVALID_POST") + + +def get_task_context_data(request): + logger.debug("[profile][get_task_context_data] Checking for tasks - " + str(request.user.id)) + + # Check for a returning task + task = request.GET.get('task') + state = request.GET.get('state') + message = request.GET.get('message') + + # Handle email confirm + if task and state and message and task == 'email_confirm': + logger.debug("[profile][get_task_context_data] Handling task '{}' - '{}' for {}".format( + task, state, request.user.id)) + + # Stash a message for the user. + if state == 'success': + messages.success(request, message, extra_tags='success', fail_silently=True) + + elif state == 'failed': + messages.error(request, message, extra_tags='danger', fail_silently=True) diff --git a/app/templates/messages.html b/app/templates/messages.html new file mode 100644 index 00000000..a934f84c --- /dev/null +++ b/app/templates/messages.html @@ -0,0 +1,32 @@ +{% load bootstrap4 %} + +{% if messages %} + +{% endif %} diff --git a/app/templates/profile/profile.html b/app/templates/profile/profile.html index 1b7bd74d..bdcf3fa6 100644 --- a/app/templates/profile/profile.html +++ b/app/templates/profile/profile.html @@ -15,6 +15,10 @@ {% block content %} + {% if messages %} + {% include 'messages.html' %} + {% endif %} +
    From 5dbb51237710f90d5a16aa21b7e87fa1f80277f1 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 13 Jan 2018 14:53:41 -0500 Subject: [PATCH 070/613] TC-51 - Passes the project identifier in the email confirm request to SciReg --- app/hypatio/scireg_services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index 9b31c7e7..9e285692 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -16,7 +16,8 @@ def send_confirmation_email(user_jwt): logger.debug("[P2M2][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) email_confirm_data = { - 'success_url': settings.EMAIL_CONFIRM_SUCCESS_URL + 'success_url': settings.EMAIL_CONFIRM_SUCCESS_URL, + 'project': 'hypatio', } requests.post(send_confirm_email_url, headers=build_headers_with_jwt(user_jwt), data=json.dumps(email_confirm_data)) From 0f508a3c014e5a4b187409e3601b5d90672c32b2 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 16 Jan 2018 09:51:11 -0500 Subject: [PATCH 071/613] TC-38: Adds links to nav bar for project management --- .gitignore | 16 ++++++++++++++++ app/datachallenges/fixtures/datachallenges.json | 2 +- app/datachallenges/views.py | 4 ++++ app/dataprojects/views.py | 7 +++++++ app/templates/base.html | 9 +++++++++ gunicorn-nginx-entry.sh | 1 + 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3f50cb09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +app/assets/ +app/.DS_Store +app/dataprojects/.DS_Store +app/db.sqlite3 +app/debug.log +app/db.sqlite3 +app/error.log +.DS_Store +app/**/.DS_Store +doc +docker-compose.override.yml +*.pyc +.idea +*__pycache__ +docker-compose.override.yml +app/hypatio/local_settings.py diff --git a/app/datachallenges/fixtures/datachallenges.json b/app/datachallenges/fixtures/datachallenges.json index 89675dc6..92ac94a1 100644 --- a/app/datachallenges/fixtures/datachallenges.json +++ b/app/datachallenges/fixtures/datachallenges.json @@ -3,7 +3,7 @@ "model": "datachallenges.datachallenge", "pk": 1, "fields": { - "name": "George Mason University", + "name": "National NLP Clinical Challenges (n2c2)", "institution": "", "description": "Long description here.", "short_description": "Short description here.", diff --git a/app/datachallenges/views.py b/app/datachallenges/views.py index 39929eb2..975458ea 100644 --- a/app/datachallenges/views.py +++ b/app/datachallenges/views.py @@ -61,10 +61,14 @@ def list_data_challenges(request, template_name='datachallenges/list.html'): "permission_scheme": data_challenge.permission_scheme} data_challenges.append(challenge) + + # TODO + is_manager = True return render(request, template_name, {"data_challenges": data_challenges, "user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) diff --git a/app/dataprojects/views.py b/app/dataprojects/views.py index 8611a723..036e3efa 100644 --- a/app/dataprojects/views.py +++ b/app/dataprojects/views.py @@ -87,6 +87,9 @@ def list_data_projects(request, template_name='dataprojects/list.html'): if user_jwt is None or validate_jwt(request) is None: logout_redirect(request) + logger.debug('[HYPATIO][DEBUG] Raw JWT: ' + user_jwt) + logger.debug('[HYPATIO][DEBUG] JWT payload: ' + json.dumps(validate_jwt(request))) + # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} @@ -141,9 +144,13 @@ def list_data_projects(request, template_name='dataprojects/list.html'): data_projects.append(project) + # TODO + is_manager = True + return render(request, template_name, {"data_projects": data_projects, "user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) diff --git a/app/templates/base.html b/app/templates/base.html index b1229a20..405b53a0 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -153,6 +153,15 @@ aria-expanded="false">{{ user.email }} diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index 0b5b9037..6355d63c 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -58,6 +58,7 @@ cd /app/ python manage.py migrate python manage.py loaddata dataprojects +python manage.py loaddata datachallenges if [ ! -d static ]; then mkdir static fi From 5ee73d317d497a8b62f31536da2eccde6c278e8c Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 18 Jan 2018 11:45:28 -0500 Subject: [PATCH 072/613] TC-51 - Updated the login URL to request email confirmation --- app/dataprojects/views.py | 16 ++++++++++++++++ app/hypatio/settings.py | 13 +++---------- app/requirements.txt | 1 + app/templates/base.html | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/dataprojects/views.py b/app/dataprojects/views.py index 8611a723..36f91a07 100644 --- a/app/dataprojects/views.py +++ b/app/dataprojects/views.py @@ -2,6 +2,7 @@ import logging import sys import requests +import furl from datetime import datetime from django.conf import settings @@ -75,6 +76,20 @@ def list_data_projects(request, template_name='dataprojects/list.html'): projects_with_view_permissions = [] projects_with_access_requests = {} + # Build the login URL + login_url = furl.furl(settings.ACCOUNT_SERVER_URL) + + # Add the next URL + login_url.args.add('next', request.build_absolute_uri()) + + # Add project, if any + project = getattr(settings, 'PROJECT', None) + if project is not None: + login_url.args.add('project', project) + + # We want email verification by default so pass that success url too + login_url.args.add('email_confirm_success_url', settings.EMAIL_CONFIRM_SUCCESS_URL) + if not request.user.is_authenticated(): user = None user_logged_in = False @@ -145,5 +160,6 @@ def list_data_projects(request, template_name='dataprojects/list.html'): "user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, + "login_url": login_url.url, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index dd4d763a..e08fd61a 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -15,7 +15,6 @@ from os.path import normpath, join, dirname, abspath from django.utils.crypto import get_random_string -from pythonpstore.pythonpstore import SecretStore chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' @@ -31,15 +30,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False -secret_store = SecretStore() -PARAMETER_PATH = os.environ.get("PS_PATH", "") +PROJECT = 'hypatio' -if PARAMETER_PATH: - ALLOWED_HOSTS = [secret_store.get_secret_for_key(PARAMETER_PATH + '.allowed_hosts')] - RAVEN_URL = secret_store.get_secret_for_key(PARAMETER_PATH + '.raven_url') -else: - ALLOWED_HOSTS = ["localhost"] - RAVEN_URL = "" +ALLOWED_HOSTS = [os.environ.get("ALLOWED_HOSTS")] # Application definition @@ -234,7 +227,7 @@ } RAVEN_CONFIG = { - 'dsn': RAVEN_URL, + 'dsn': os.environ.get("RAVEN_URL", ""), # If you are using git, you can also automatically configure the # release based on the git info. 'release': '1', diff --git a/app/requirements.txt b/app/requirements.txt index f200c394..e69d6ff1 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -5,6 +5,7 @@ django-stronghold==0.2.8 djangorestframework==3.5.3 djangorestframework-jwt==1.9.0 django-smtp-ssl==1.0 +furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 py-auth0-jwt==0.2.5 diff --git a/app/templates/base.html b/app/templates/base.html index 729d54d7..ea8b6bbc 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -165,7 +165,7 @@ {% else %}
  • - +   Register / Sign In From 0594270427cff510f8720d1a27626172f1476bee Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 18 Jan 2018 15:05:43 -0500 Subject: [PATCH 073/613] TC-38: Adds a screen for project authorization and permission granting This commit adds a Manage Contests screen where a user with MANAGE permissions on a given item (for now just "N2C2") can view all the users who have requested authorization to a project. The user can then grant the user authorization to the project and VIEW permissions at the click of a button. Calls will be made to SciAuthZ to do this. Also downgrades from bootstrap4 to 3 for better compatibility with existing Hypatio functionality. --- .gitignore | 19 +-- app/datachallenges/urls.py | 4 +- app/datachallenges/views.py | 160 +++++++++++++++++- app/dataprojects/views.py | 19 ++- app/hypatio/settings.py | 8 +- app/profile/views.py | 32 ++++ app/requirements.txt | 2 +- app/templates/base.html | 36 ++-- app/templates/contact/contact.html | 2 +- app/templates/datachallenges/list.html | 2 +- .../datachallenges/managecontests.html | 82 +++++++++ app/templates/dataprojects/list.html | 2 +- app/templates/profile/profile.html | 2 +- 13 files changed, 315 insertions(+), 55 deletions(-) create mode 100644 app/templates/datachallenges/managecontests.html diff --git a/.gitignore b/.gitignore index 3f50cb09..ca576e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,5 @@ -app/assets/ -app/.DS_Store -app/dataprojects/.DS_Store -app/db.sqlite3 -app/debug.log -app/db.sqlite3 -app/error.log -.DS_Store -app/**/.DS_Store -doc -docker-compose.override.yml +app/assets/* *.pyc -.idea -*__pycache__ -docker-compose.override.yml -app/hypatio/local_settings.py +*/.DS_Store +.DS_Store +*.log \ No newline at end of file diff --git a/app/datachallenges/urls.py b/app/datachallenges/urls.py index 90481aeb..aca4d266 100644 --- a/app/datachallenges/urls.py +++ b/app/datachallenges/urls.py @@ -1,8 +1,10 @@ from django.conf.urls import url -from .views import list_data_challenges, signout +from .views import list_data_challenges, signout, manage_contests, grant_access_with_view_permissions urlpatterns = ( url(r'^$', list_data_challenges), + url(r'^grantviewpermissions', grant_access_with_view_permissions), url(r'^list/$', list_data_challenges), + url(r'^managecontests/$', manage_contests), url(r'^signout/$', signout), ) diff --git a/app/datachallenges/views.py b/app/datachallenges/views.py index 975458ea..00c816c8 100644 --- a/app/datachallenges/views.py +++ b/app/datachallenges/views.py @@ -11,6 +11,8 @@ from .models import DataChallenge +from profile.views import user_has_manage_permission + from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from django.contrib import messages @@ -35,6 +37,8 @@ def list_data_challenges(request, template_name='datachallenges/list.html'): all_data_challenges = DataChallenge.objects.all() data_challenges = [] + is_manager = False + if not request.user.is_authenticated(): user = None user_logged_in = False @@ -47,6 +51,9 @@ def list_data_challenges(request, template_name='datachallenges/list.html'): if user_jwt is None or validate_jwt(request) is None: logout_redirect(request) + # TODO Does this user have MANAGE permissions on any item? + is_manager = user_has_manage_permission(request, 'N2C2') + # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} @@ -55,15 +62,12 @@ def list_data_challenges(request, template_name='datachallenges/list.html'): # Package all the necessary information into one dictionary challenge = {"name": data_challenge.name, - "short_description": data_challenge.short_description, - "description": data_challenge.description, - "challenge_key": data_challenge.challenge_key, - "permission_scheme": data_challenge.permission_scheme} + "short_description": data_challenge.short_description, + "description": data_challenge.description, + "challenge_key": data_challenge.challenge_key, + "permission_scheme": data_challenge.permission_scheme} data_challenges.append(challenge) - - # TODO - is_manager = True return render(request, template_name, {"data_challenges": data_challenges, "user_logged_in": user_logged_in, @@ -72,3 +76,145 @@ def list_data_challenges(request, template_name='datachallenges/list.html'): "is_manager": is_manager, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) + +@user_auth_and_jwt +def manage_contests(request, template_name='datachallenges/managecontests.html'): + + all_data_challenges = DataChallenge.objects.all() + data_challenges = [] + + # TODO eventually this shouldn't be hard coded for n2c2 + data_challenge = "N2C2" + + # This dictionary will hold all user requests and permissions + user_details = {} + + user = request.user + user_logged_in = True + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + # If the JWT has expired or the user doesn't have one, force the user to login again + if user_jwt is None or validate_jwt(request) is None: + logout_redirect(request) + + # Confirm that the user has MANAGE permissions for this item + is_manager = user_has_manage_permission(request, data_challenge) + + if not is_manager: + logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + data_challenge + '.') + return HttpResponse(403) + + # The JWT token that will get passed in API calls + jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + authorization_request_url = settings.AUTHORIZATION_REQUEST_URL + "?item=" + data_challenge + logger.debug('[HYPATIO][DEBUG] authorization_request_url: ' + authorization_request_url) + + # Query SciAuthZ for all access requests to this contest + authorization_requests = requests.get(authorization_request_url, headers=jwt_headers, verify=False).json() + logger.debug('[HYPATIO][DEBUG] Item Permission Requests: ' + json.dumps(authorization_requests)) + + if authorization_requests is not None and 'results' in authorization_requests: + authorization_requests_json = authorization_requests['results'] + + for authorization_request in authorization_requests_json: + + date_requested = "" + date_request_granted = "" + + if authorization_request['date_requested'] is not None: + date_requested = datetime.strftime(datetime.strptime(authorization_request['date_requested'], "%Y-%m-%dT%H:%M:%S"), "%b %d %Y, %H:%M:%S") + + if authorization_request['date_request_granted'] is not None: + date_request_granted = datetime.strftime(datetime.strptime(authorization_request['date_request_granted'], "%Y-%m-%dT%H:%M:%S"), "%b %d %Y, %H:%M:%S") + + # Store the permission request in a dictionary and include a blank permissions key to be added later + user_detail = {'personal_information': {'first_name': '', + 'last_name': ''}, + 'authorization_request': {'date_requested': date_requested, + 'request_granted': authorization_request['request_granted'], + 'date_request_granted': date_request_granted, + 'request_id': authorization_request['id']}, + 'permissions': []} + + # Add a key to the user details dictionary with the user email as the key and permis + user_details[authorization_request['user']] = user_detail + + # Query SciAuthZ for all permissions to this contest + permissions_url = settings.USER_PERMISSIONS_URL + "?item=" + data_challenge + user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() + logger.debug('[HYPATIO][DEBUG] User Permissions: ' + json.dumps(user_permissions)) + + if user_permissions is not None and 'results' in user_permissions: + user_permissions = user_permissions["results"] + + for permission in user_permissions: + + # If this user is not already in the user detail dictionary, add them (user does not have a permission request apparently) + if permission['user'] not in user_details: + user_details[permission['user']] = {'personal_information': {'first_name': '', + 'last_name': ''}, + 'authorization_request': '', + 'permissions': ''} + + # Add this permission to the user details dictionary + user_details[permission['user']]['permissions'].append(permission['permission']) + + logger.debug('[HYPATIO][DEBUG] user_details: ' + json.dumps(user_details)) + + return render(request, template_name, {"user_logged_in": user_logged_in, + "user": user, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "user_details": user_details, + "project": data_challenge}) + +@user_auth_and_jwt +def grant_access_with_view_permissions(request): + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + project = request.POST['project'] + person_email = request.POST['user_email'] + authorization_request_id = request.POST['authorization_request_id'] + + current_time = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%S") + + logger.debug('[HYPATIO][DEBUG] Granting authorization request ' + authorization_request_id + ' and view permissions to ' + person_email + ' for project ' + project + '.') + + is_manager = user_has_manage_permission(request, project) + + if not is_manager: + logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + project + '.') + return HttpResponse(403) + + # Grab the full authorization request object from SciAuthZ to have all fields necessary for serialization + authorization_request_url = settings.AUTHORIZATION_REQUEST_URL + "?id=" + authorization_request_id + authorization_request = requests.get(authorization_request_url, headers=jwt_headers, verify=False).json() + + if authorization_request is not None and 'results' in authorization_request: + authorization_request_data = authorization_request['results'][0] + logger.debug('[HYPATIO][DEBUG] authorization_request_data ' + json.dumps(authorization_request_data)) + else: + # This authorization request should have been found + return HttpResponse(404) + + # Update the granted flag in the authorization request + authorization_request_data['request_granted'] = True + authorization_request_data['date_request_granted'] = current_time + + authorization_request_url = settings.AUTHORIZATION_REQUEST_GRANT_URL + authorization_request_id + '/' + requests.put(authorization_request_url, headers=jwt_headers, data=json.dumps(authorization_request_data), verify=False) + + user_permission = {"user": person_email, + "item": project, + "permission": "VIEW", + "date_updated": current_time} + + # Add a VIEW permission to the user + permissions_url = settings.USER_PERMISSIONS_URL + user_permissions = requests.post(permissions_url, headers=jwt_headers, data=json.dumps(user_permission), verify=False) + + return HttpResponse(200) diff --git a/app/dataprojects/views.py b/app/dataprojects/views.py index 036e3efa..e6f12ca3 100644 --- a/app/dataprojects/views.py +++ b/app/dataprojects/views.py @@ -11,6 +11,8 @@ from .models import DataProject, DataUseAgreement, DataUseAgreementSign +from profile.views import user_has_manage_permission + from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from django.contrib import messages @@ -61,7 +63,7 @@ def submit_request(request): "item": request.POST['project_key']} # Send the authorization request to SciAuthZ - create_auth_request_url = settings.CREATE_REQUEST_URL + create_auth_request_url = settings.AUTHORIZATION_REQUEST_URL requests.post(create_auth_request_url, headers=jwt_headers, data=json.dumps(data_request), verify=False) return HttpResponse(200) @@ -75,6 +77,8 @@ def list_data_projects(request, template_name='dataprojects/list.html'): projects_with_view_permissions = [] projects_with_access_requests = {} + is_manager = False + if not request.user.is_authenticated(): user = None user_logged_in = False @@ -87,14 +91,14 @@ def list_data_projects(request, template_name='dataprojects/list.html'): if user_jwt is None or validate_jwt(request) is None: logout_redirect(request) - logger.debug('[HYPATIO][DEBUG] Raw JWT: ' + user_jwt) - logger.debug('[HYPATIO][DEBUG] JWT payload: ' + json.dumps(validate_jwt(request))) + # TODO Does this user have MANAGE permissions on any item? + is_manager = user_has_manage_permission(request, 'N2C2') # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} # Get all of the user's VIEW permissions - permissions_url = settings.PERMISSION_SERVER + permissions_url = settings.USER_PERMISSIONS_URL + "?email=" + user.email user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() logger.debug('[HYPATIO][DEBUG] User Permissions: ' + json.dumps(user_permissions)) @@ -106,7 +110,9 @@ def list_data_projects(request, template_name='dataprojects/list.html'): projects_with_view_permissions.append(user_permission['item']) # Get all of the user's permission requests - access_requests_url = settings.GET_ACCESS_REQUESTS + access_requests_url = settings.AUTHORIZATION_REQUEST_URL + "?email=" + user.email + logger.debug('[HYPATIO][DEBUG] access_requests_url: ' + access_requests_url) + user_access_requests = requests.get(access_requests_url, headers=jwt_headers, verify=False).json() logger.debug('[HYPATIO][DEBUG] User Permission Requests: ' + json.dumps(user_access_requests)) @@ -144,9 +150,6 @@ def list_data_projects(request, template_name='dataprojects/list.html'): data_projects.append(project) - # TODO - is_manager = True - return render(request, template_name, {"data_projects": data_projects, "user_logged_in": user_logged_in, "user": user, diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index e221748b..0ed41750 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -51,7 +51,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'jquery', - 'bootstrap4', + 'bootstrap3', 'contact', 'datachallenges', 'dataprojects', @@ -136,9 +136,9 @@ SCIREG_SERVER_URL = os.environ.get("SCIREG_SERVER_URL") AUTHZ_BASE = os.environ.get("AUTHZ_BASE", "") -PERMISSION_SERVER = AUTHZ_BASE + "/user_permission/" -CREATE_REQUEST_URL = AUTHZ_BASE + "/authorization_requests/" -GET_ACCESS_REQUESTS = AUTHZ_BASE + "/authorization_requests/" +USER_PERMISSIONS_URL = AUTHZ_BASE + "/user_permission/" +AUTHORIZATION_REQUEST_URL = AUTHZ_BASE + "/authorization_requests/" +AUTHORIZATION_REQUEST_GRANT_URL = AUTHZ_BASE + "/authorization_request_change/" COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN") diff --git a/app/profile/views.py b/app/profile/views.py index e9dda0fb..b91a6bec 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -31,6 +31,9 @@ def profile(request, template_name='profile/profile.html'): if user_jwt is None or validate_jwt(request) is None: logout_redirect(request) + # TODO Does this user have MANAGE permissions on any item? + is_manager = user_has_manage_permission(request, 'N2C2') + # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} @@ -54,6 +57,7 @@ def profile(request, template_name='profile/profile.html'): # Generate and render the form. return render(request, template_name, {'form': form, 'user': user, + 'is_manager': is_manager, 'new_user': False, 'user_logged_in': user_logged_in}) else: @@ -82,5 +86,33 @@ def profile(request, template_name='profile/profile.html'): # Generate and render the form. return render(request, template_name, {'form': form, 'user': user, + 'is_manager': is_manager, 'new_user': new_user, 'user_logged_in': user_logged_in}) + +# Check if this user has SciAuthZ manage permissions on the given item +def user_has_manage_permission(request, item): + + is_manager = False + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + # If the JWT has expired or the user doesn't have one, force the user to login again + if user_jwt is None or validate_jwt(request) is None: + logout_redirect(request) + + jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + # Confirm user is a manager of the given project + permissions_url = settings.USER_PERMISSIONS_URL + "?item=" + item + "&email=" + user.email + user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() + # logger.debug('[HYPATIO][DEBUG] permissions_url: ' + permissions_url) + # logger.debug('[HYPATIO][DEBUG] user_permissions: ' + json.dumps(user_permissions)) + + if user_permissions is not None and 'results' in user_permissions: + for perm in user_permissions['results']: + if perm['permission'] == "MANAGE": + is_manager = True + + return is_manager \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt index f200c394..5fcb0900 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,5 @@ Django==1.10.1 -django-bootstrap4==0.0.5 +django-bootstrap3==9.0.0 django-jquery==3.1.0 django-stronghold==0.2.8 djangorestframework==3.5.3 diff --git a/app/templates/base.html b/app/templates/base.html index 405b53a0..04338983 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,3 +1,5 @@ + + {% load static %} {% load contact_extras %} @@ -6,11 +8,10 @@ - - + + {% load bootstrap3 %} + {% bootstrap_css %} + {% bootstrap_javascript %} @@ -110,7 +111,7 @@ - {% block scripts %} + {% block headscripts %} {% endblock %} @@ -159,7 +160,7 @@
  • Manage Projects
  • -
  • Manage Contests
  • +
  • Manage Contests
  • {% endif %} @@ -184,15 +185,17 @@
    - From 07f92d0665eaf04cfcae754ef19b09cececb8c51 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Wed, 7 Feb 2018 21:22:45 -0500 Subject: [PATCH 109/613] TC-122 - Changing contests to challenges. --- app/projects/fixtures/projects.json | 2 +- app/templates/datacontests/managecontests.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index 21551731..382072fe 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -29,7 +29,7 @@ "model": "projects.dataproject", "pk": 6, "fields": { - "name": "National NLP Clinical Contests (n2c2)", + "name": "National NLP Clinical Challenges (n2c2)", "institution": 1, "description": "

    Tentative Timeline

    \n

    February 19 — Registration opens\n
    March 5 — Training data release\n
    April 30 — Test data release\n
    May 2 — System outputs due\n
    June 2 — Abstracts due\n
    TBD — Workshop

    \n
    \n

    Task Description

    \n

    This task aims to answer the question, “Can NLP systems use narrative medical records to identify which patients meet selection criteria for clinical trials?” The task requires NLP systems to compare each patient to a list of selection criteria, and determine if the patients meet, do not meet, or possibly meet each criterion.

    \n

    Definitions and background

    \n

    Identifying patients who meet certain criteria for placement in clinical trials is a vital part of medical research. Finding patients for clinical trials is a challenge, as medical studies often have complex criteria that cannot easily be translated into a database query, but rather require examining the clinical narratives in a patient’s records. This is time-consuming for medical researchers who need to recruit patients, so often researchers are limited to patients who either seek out the trial for themselves, or who are pointed towards a particular trial by their doctor. Recruitment from particular places or by particular people can result in selection bias towards certain populations (e.g., people who can afford regular care, or people who exclusively use free clinics), which in turn can bias the results of the study (Mann, 2003 ; Genletti et al, 2009 ). Developing NLP systems that can automatically assess if a patient is eligible for a study can both reduce the time it takes to recruit patients, and help remove bias from clinical trials (Stubbs, 2013 ).

    \n

    However, matching patients to selection criteria is not a trivial task for machines, due to the complexity the criteria often exhibit. For example, consider the phrase “Patient must be taking aspirin for MI prevention.” In this case, it is insufficient for a system to extract only the medication (i.e., aspirin); rather, the system must also determine the reason why the patient is taking the medication (i.e., for MI prevention). This shared task aims to identify whether a patient meets, does not meet, or possibly meets a selected set of eligibility criteria based on their longitudinal records. The eligibility criteria come from real clinical trials and focus on patients’ medications, past medical histories, and whether certain events have occurred in a specified timeframe in the patients’ records.

    \n

    Data

    \n

    This task uses data from the 2014 i2b2/UTHealth Shared-Tasks and Workshop on Challenges in Natural Language Processing for Clinical Data, with tasks on de-identification and heart disease risk factors (learn more ). The data consists of nearly 300 sets of longitudinal patient records, annotated by medical professionals to determine if each patient matches a list of 13 selection criteria. These criteria include determining whether the patient has taken a dietary supplement (excluding Vitamin D) in the past 2 months, whether the patient has a major diabetes-related complication, and whether the patient has advanced cardiovascular disease.

    \n

    All the files have been annotated to indicate whether the patient meets, does not meet, or possibly meets each criterion. The gold standard annotations will provide the category of each patient for each criterion, as well as the spans of text that the annotators used to support their annotations. Participants will be evaluated on the predicted category of each patient in the held-out test data.

    \n

    The data for this task is provided by Partners HealthCare. All records have been fully de-identified and manually annotated for whether they meet, possibly meet, or do not meet clinical trial eligibility criteria.

    \n

    Data for the challenge will be released under a Rules of Conduct and Data Use Agreement. Obtaining the data requires completing a registration, which will start February 19, 2018.

    \n

    Evaluation Format

    \n

    The evaluation for both NLP tasks will be conducted using withheld test data. Participating teams are asked to stop development as soon as they download the test data. Each team is allowed to upload (through this website) up to three system runs for each of these tracks. System output is to be submitted in the exact format of the ground truth annotations, which will be provided by the organizers.

    \n

    Dissemination

    \n

    Participants are asked to submit a 500-word long abstract describing their methodologies. Abstracts may also have a graphical summary of the proposed architecture. The document should not exceed 2 pages (i.e., 1.5\" line spacing, 12pt-font size). The authors of either top performing systems or particularly novel approaches will be invited to present or demonstrate their systems at the workshop. A special issue of a journal will be organized following the workshop.

    \n

    Organizing Committee

    \n
      \n
    • Ozlem Uzuner, co-chair, George Mason University
    • \n
    • Amber Stubbs, co-chair, Simmons College
    • \n
    • Michele Filannino, co-chair, MIT
    • \n
    • Kevin Buchan, SUNY at Albany
    • \n
    • Susanne Churchill, Harvard Medical School
    • \n
    • Isaac Kohane, Harvard Medical School
    • \n
    • Hua Xu, UTHealth
    • \n
    • Ergin Soysal, UTHealth
    • \n
    \n

    References

    \n

    Sara Geneletti, Sylvia Richardson, and Nicky Best. 2009. Adjusting for selection bias in retrospective, case-control studies. Biostatistics, 10(1):17–31.

    \n

    C. J. Mann. 2003. Observational research methods. Research design II: cohort, cross sectional, and case-control studies . Emergency Medical Journal, 20:54–60.

    \n

    Stubbs, A. A Methodology for Using Professional Knowledge in Corpus Annotation. PhD Thesis, Brandeis University, 2013.

    \n

     

    ", "short_description": "2018 Track 1: Cohort Selection for Clinical Trials", diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index a0ea494e..ab485a2a 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -3,7 +3,7 @@ {% block headscripts %} {% endblock %} -{% block title %}Manage Data Contests{% endblock %} +{% block title %}Manage Data Challenges{% endblock %} {% block subtitle %}Temporary page to manage N2C2{% endblock %} {% block content %} From 93ada18f8109c04f8e95e0cf45fbd478736296e3 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 8 Feb 2018 11:18:59 -0500 Subject: [PATCH 110/613] TC-121: Verify Email button will redirect user back to place where they originally initiated registration --- app/hypatio/scireg_services.py | 4 +-- app/profile/views.py | 2 +- app/projects/templatetags/projects_extras.py | 4 +-- app/projects/views.py | 4 +++ app/templates/profile/verify_email.html | 26 ++++++++++---------- app/templates/project_details.html | 5 ++++ 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index 9e285692..a8f9db90 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -10,13 +10,13 @@ def build_headers_with_jwt(user_jwt): return {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} -def send_confirmation_email(user_jwt): +def send_confirmation_email(user_jwt, current_uri): send_confirm_email_url = settings.SCIREG_SERVER_URL + '/api/register/send_confirmation_email/' logger.debug("[P2M2][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) email_confirm_data = { - 'success_url': settings.EMAIL_CONFIRM_SUCCESS_URL, + 'success_url': current_uri, 'project': 'hypatio', } diff --git a/app/profile/views.py b/app/profile/views.py index 153b07c9..6d37d0e7 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -168,7 +168,7 @@ def send_confirmation_email_view(request): recaptcha_response = recaptcha_check(request) if recaptcha_response["status"]: - scireg_services.send_confirmation_email(request.COOKIES.get("DBMI_JWT", None)) + scireg_services.send_confirmation_email(request.COOKIES.get("DBMI_JWT", None), request.POST.get('current_uri')) return HttpResponse("SENT") else: return HttpResponse("FAILED_RECAPTCHA") diff --git a/app/projects/templatetags/projects_extras.py b/app/projects/templatetags/projects_extras.py index 47bfe782..c3ac3a50 100644 --- a/app/projects/templatetags/projects_extras.py +++ b/app/projects/templatetags/projects_extras.py @@ -27,7 +27,7 @@ def get_login_url(current_uri): if project is not None: login_url.args.add('project', project) - # We want email verification by default so pass that success url too - login_url.args.add('email_confirm_success_url', settings.EMAIL_CONFIRM_SUCCESS_URL) + # Pass the current URI to SciAuth, which it will use to redirect users who verify their emails + login_url.args.add('email_confirm_success_url', current_uri) return login_url.url diff --git a/app/projects/views.py b/app/projects/views.py index f9a251aa..5cb8022d 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -22,6 +22,7 @@ from .models import SignedAgreementForm from profile.views import user_has_manage_permission +from profile.views import get_task_context_data from profile.forms import RegistrationForm from django.http import HttpResponse @@ -432,6 +433,9 @@ def project_details(request, project_key, template_name='project_details.html'): # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + # Check for a returning task and set messages accordingly + get_task_context_data(request) + # Make a request to SciReg to grab email verification and profile information profile_registration_url = settings.SCIREG_SERVER_URL + '/api/register/' profile_registration_info = requests.get(profile_registration_url, headers=jwt_headers, verify=False).json() diff --git a/app/templates/profile/verify_email.html b/app/templates/profile/verify_email.html index 92a28550..66b8f2e7 100644 --- a/app/templates/profile/verify_email.html +++ b/app/templates/profile/verify_email.html @@ -3,6 +3,9 @@
    {% csrf_token %} + {# Pass the current URI ultimately onto the email verification button #} + +
    @@ -28,19 +31,16 @@ // Show the loading animation $('#submit_verification_email').button('loading'); - $.post("/profile/send_confirmation_email/", - $('#confirm_email_form').serialize(), - function( data ) { - - if(data == "SENT") { - notify('success', "E-Mail Sent.", 'thumbs-up'); + $.post("/profile/send_confirmation_email/", $('#confirm_email_form').serialize(), function( data ) { + if(data == "SENT") { + notify('success', "E-Mail Sent.", 'thumbs-up'); - // Update the html. - $('#submit_verification_email').replaceWith('The confirmation e-mail has been sent! Check your inbox for instructions on how to complete the e-mail verification process'); - } - else if(data == "FAILED_RECAPTCHA") {notify('danger', "Failed to verify reCAPTCHA, please refresh page and try again.");} - else if(data == "INVALID_POST") {notify('danger', "Error processing request.");} - }) - } + // Update the html. + $('#submit_verification_email').replaceWith('The confirmation e-mail has been sent! Check your inbox for instructions on how to complete the e-mail verification process'); + } + else if(data == "FAILED_RECAPTCHA") {notify('danger', "Failed to verify reCAPTCHA, please refresh page and try again.");} + else if(data == "INVALID_POST") {notify('danger', "Error processing request.");} + }); + }; {% endblock %} \ No newline at end of file diff --git a/app/templates/project_details.html b/app/templates/project_details.html index f07f1f19..e99e97c8 100644 --- a/app/templates/project_details.html +++ b/app/templates/project_details.html @@ -9,6 +9,11 @@ {% block subtitle %}{{ project.short_description }}{% endblock %} {% block content %} + +{% if messages %} +{% include 'messages.html' %} +{% endif %} +
    From 9bc87a147b1eb7483e8dabf0fef5275e5d7fe752 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 8 Feb 2018 11:28:27 -0500 Subject: [PATCH 111/613] TC-120: Adds favicon and DBMI Portal title --- app/static/favicon.ico | Bin 0 -> 7358 bytes app/templates/base.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 app/static/favicon.ico diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..29b0158a65a98398ee00e69058a91d0f6806cdb0 GIT binary patch literal 7358 zcmeI1y>=8a426f9&U+4rf(OX&W?zRc6&)Q_D(KRsN|!Da(9xyZ3LcElx?_#T_Uvu~ zBn@O@JhF6k^<$3@48tpaHk*O*?eOj0FuWOt;fP;E!zI62p8tGiZS`8eCScg4Uw{5w zq=%FA=_tL|bDF*!f{7LS7#^bh1f6yI4wK__iI>xKwMpB(Apew)ykDdrqQnTj^K=JK zo|fA>-5<{9EM2!To~K*f6~7O7JV_s0Usd^V8XsUG2@bZ%4=k<_D6XQz1rrI^t2?8# zRUQz6--eIJvvh-ZK>TqNK`q&~XvgV;n`q(^FrkMD?p&wMQi~}G4BZOMsOHI!Le1sN z@^ep_gup9xWKOEdiR6sHI>{EQk^tg-swjZ1l*4N-2MS8XY0mt(i>sW&T%|;r`HmO` zuKWN}Rhd_K1_Jd#q*R~-lX)OpY;mpL5CM-`iGmwRbB_eY92^zh!W{R76VQ^swi87S zzBC!(FgsONQwUO4A&g8ug?O-%d3n98LF_~ZqTom;wzMhl!r-_m-&{ZQVzCW=W7KS6wr^QFXmG@ zq^1Dd)jg8X!)VeKr7j#DH6;;7hX-ObcB_L#L22CMeb;SdOONRhCEQg_t6ifm6*p~p zC&rTRz9;4%9r?%2YHkUt7;o`pshK=$L0qz&Vd%iJMOH* z$FmNnQ3qvjKkqkfI+l#U*oT@wmJbAQ2cTldd)#_lX@C`ByhE5oxeIJrGD+?k5J(zq zeeRl1n|L0WL@SjQZ%~@wdV2yZJp%K!OER8V+}AKYf|pks70%4Ed(a? zm3Nwo8_t~kO^M$u@j)$CN9U4wE^&x*JdOx;15nU>y*H?Qdla%JvHkqAL8XXG9SE#@ zaU7TIm`r@a2|(cL7RH!5d{H&qeO%bC-BHvjw5J=9O19iWxs!PeJmZsr*n})8EljFF zd14!ht$bCC!1_*32nkbV##

    j5@Y``QGH2{hbcov@fb2=GI6J7>t;Zu#9gSubl^nS z1O^<4|5$wVpTWgn_F@;# hCq(++ + + DBMI Portal + \ No newline at end of file From 32b2815c544db2e81874957a69866653f19b8b07 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Sat, 10 Feb 2018 17:58:22 -0500 Subject: [PATCH 118/613] TC-114 - Refactoring SciAuthZ code, reviving basic project signups. --- app/hypatio/sciauthz_services.py | 73 +++++++++++++ app/profile/views.py | 39 ++----- app/projects/admin.py | 5 + app/projects/fixtures/participants.json | 17 +++ app/projects/fixtures/projects.json | 56 ++++++---- .../migrations/0013_auto_20180208_2102.py | 27 +++++ .../migrations/0014_auto_20180208_2107.py | 20 ++++ .../migrations/0015_auto_20180208_2107.py | 20 ++++ app/projects/migrations/0016_datagate.py | 25 +++++ app/projects/models.py | 17 +++ app/projects/templatetags/projects_extras.py | 40 ++++++- app/projects/views.py | 101 +++++------------- .../dataprojects/access_request.html | 42 ++++---- app/templates/dataprojects/blurb.html | 36 ++++++- 14 files changed, 366 insertions(+), 152 deletions(-) create mode 100644 app/hypatio/sciauthz_services.py create mode 100644 app/projects/migrations/0013_auto_20180208_2102.py create mode 100644 app/projects/migrations/0014_auto_20180208_2107.py create mode 100644 app/projects/migrations/0015_auto_20180208_2107.py create mode 100644 app/projects/migrations/0016_datagate.py diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py new file mode 100644 index 00000000..fa243695 --- /dev/null +++ b/app/hypatio/sciauthz_services.py @@ -0,0 +1,73 @@ +from json import JSONDecodeError + +import requests +import json + + +class SciAuthZ: + USER_PERMISSIONS_URL = None + JWT_HEADERS = None + CURRENT_USER_EMAIL = None + AUTHORIZATION_REQUEST_URL = None + + VERIFY_REQUEST = False + + def __init__(self, authz_base, jwt, user_email): + + user_permissions_url = authz_base + "/user_permission/" + authorization_request_url = authz_base + "/authorization_requests/" + authorization_request_grant_url = authz_base + "/authorization_request_change/" + + self.USER_PERMISSIONS_URL = user_permissions_url + self.AUTHORIZATION_REQUEST_URL = authorization_request_url + jwt_headers = {"Authorization": "JWT " + jwt, 'Content-Type': 'application/json'} + + self.JWT_HEADERS = jwt_headers + self.CURRENT_USER_EMAIL = user_email + self.AUTHORIZATION_REQUEST_GRANT_URL = authorization_request_grant_url + + # Check if this user has SciAuthZ manage permissions on the given item + def user_has_manage_permission(self, request, item): + + is_manager = False + + # Confirm user is a manager of the given project + permissions_url = self.USER_PERMISSIONS_URL + "?item=" + item + "&email=" + self.CURRENT_USER_EMAIL + + try: + user_permissions = requests.get(permissions_url, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + except JSONDecodeError: + user_permissions = None + + if user_permissions is not None and 'results' in user_permissions: + for perm in user_permissions['results']: + if perm['permission'] == "MANAGE": + is_manager = True + + return is_manager + + def current_user_permissions(self): + + try: + user_permissions = requests.get(self.USER_PERMISSIONS_URL + "?email=" + self.CURRENT_USER_EMAIL, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + except JSONDecodeError: + user_permissions = None + + return user_permissions + + def current_user_access_requests(self): + + try: + user_access_requests = requests.get(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + except JSONDecodeError: + user_access_requests = None + + return user_access_requests + + def current_user_request_access(self, access_request): + try: + user_access_request = requests.post(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, data=json.dumps(access_request), verify=self.VERIFY_REQUEST) + except JSONDecodeError: + user_access_request = None + + return user_access_request \ No newline at end of file diff --git a/app/profile/views.py b/app/profile/views.py index 6d37d0e7..26b3f221 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -2,6 +2,8 @@ import logging import requests + + from django.contrib import messages from django.conf import settings from django.shortcuts import render @@ -9,6 +11,8 @@ from .forms import RegistrationForm from django.http import HttpResponse +from hypatio.sciauthz_services import SciAuthZ + from hypatio import scireg_services @@ -60,12 +64,8 @@ def profile(request, template_name='profile/profile.html'): user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - # If the JWT has expired or the user doesn't have one, force the user to login again - if user_jwt is None or validate_jwt(request) is None: - logout_redirect(request) - - # TODO Does this user have MANAGE permissions on any item? - is_manager = user_has_manage_permission(request, 'n2c2-t1') + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, 'n2c2-t1') # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} @@ -96,33 +96,6 @@ def profile(request, template_name='profile/profile.html'): 'new_user': new_user, 'user_logged_in': user_logged_in}) -# Check if this user has SciAuthZ manage permissions on the given item -def user_has_manage_permission(request, item): - - is_manager = False - - user = request.user - user_jwt = request.COOKIES.get("DBMI_JWT", None) - - # If the JWT has expired or the user doesn't have one, force the user to login again - if user_jwt is None or validate_jwt(request) is None: - logout_redirect(request) - - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} - - # Confirm user is a manager of the given project - permissions_url = settings.USER_PERMISSIONS_URL + "?item=" + item + "&email=" + user.email - user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() - # logger.debug('[HYPATIO][DEBUG] permissions_url: ' + permissions_url) - # logger.debug('[HYPATIO][DEBUG] user_permissions: ' + json.dumps(user_permissions)) - - if user_permissions is not None and 'results' in user_permissions: - for perm in user_permissions['results']: - if perm['permission'] == "MANAGE": - is_manager = True - - return is_manager - def get_client_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: diff --git a/app/projects/admin.py b/app/projects/admin.py index 5315ea60..0031454a 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -5,6 +5,7 @@ from .models import Team from .models import Participant from .models import Institution +from .models import DataGate class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') #, 'project_url') @@ -24,9 +25,13 @@ class ParticipantAdmin(admin.ModelAdmin): class InstitutionAdmin(admin.ModelAdmin): list_display = ('name', 'logo') +class DataGateAdmin(admin.ModelAdmin): + list_display = ('project', 'data_location_type', 'data_location') + admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) admin.site.register(SignedAgreementForm, SignedagreementformAdmin) admin.site.register(Team, TeamAdmin) admin.site.register(Participant, ParticipantAdmin) admin.site.register(Institution, InstitutionAdmin) +admin.site.register(DataGate, DataGateAdmin) diff --git a/app/projects/fixtures/participants.json b/app/projects/fixtures/participants.json index 10a2f12a..407efbad 100644 --- a/app/projects/fixtures/participants.json +++ b/app/projects/fixtures/participants.json @@ -15,6 +15,14 @@ "password": "password" } }, + { "model": "auth.user", + "pk": 102, + "fields": { + "username": "test3", + "email": "test3@test.com", + "password": "password" + } + }, { "model": "projects.participant", "pk": 100, "fields": { @@ -29,6 +37,15 @@ "data_challenge": [6] } }, + { "model": "projects.participant", + "pk": 102, + "fields": { + "user": 102, + "data_challenge": [6], + "team_wait_on_pi": true, + "team_wait_on_pi_email": "mcduffie.michael@gmail.com" + } + }, { "model": "projects.team", "pk": 100, "fields": { diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index 382072fe..c4eebcb1 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -53,7 +53,35 @@ "permission_scheme": "PRIVATE", "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": false + "is_contest": false, + "visible": true + } + }, + { + "model": "projects.datagate", + "pk": 1, + "fields": { + "project": 2, + "data_location_type": "EXTERNAL_APP_URL", + "data_location": "https://nhanes.hms.harvard.edu/" + } + }, + { + "model": "projects.datagate", + "pk": 2, + "fields": { + "project": 3, + "data_location_type": "EXTERNAL_APP_URL", + "data_location": "https://grdr.hms.harvard.edu/" + } + }, + { + "model": "projects.datagate", + "pk": 3, + "fields": { + "project": 4, + "data_location_type": "EXTERNAL_APP_URL", + "data_location": "https://ssc.hms.harvard.edu/" } }, { @@ -68,7 +96,8 @@ "permission_scheme": "PUBLIC", "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": false + "is_contest": false, + "visible": true } }, { @@ -83,7 +112,8 @@ "permission_scheme": "PUBLIC", "agreement_forms_required": true, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": false + "is_contest": false, + "visible": true } }, { @@ -92,28 +122,14 @@ "fields": { "name": "SSC i2b2/tranSMART", "institution": null, - "description": "

    Description

    ", + "description": "
    Simons Simplex Collection is a dataset consisting of genetic samples from 2,600 families with a child affected with an autism spectrum disorder. This data has been loaded into an i2b2/tranSMART instance and is available on an invitation only basis.

    ", "short_description": "Simons Simplex Collection", "project_key": "SSC", "permission_scheme": "PUBLIC", - "agreement_forms_required": false, - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": false - } - }, - { - "model": "projects.dataproject", - "pk": 5, - "fields": { - "name": "i2b2 NLP Research Data Sets", - "institution": null, - "description": "
    i2b2 is a passionate advocate for the potential of existing clinical information to yield insights that can directly impact healthcare improvement. In our many use cases (Driving Biology Projects) it has become increasingly obvious that the value locked in unstructured text is essential to the success of our mission. In order to enhance the ability of natural language processing (NLP) tools to prise increasingly fine grained information from clinical records, i2b2 has previously provided sets of fully deidentified notes from the Research Patient Data Repository at Partners HealthCare for a series of NLP Challenges organized by Dr. Ozlem Uzuner. We are pleased to now make those notes available to the community for general research purposes. At this time we are releasing the notes (~1,500) from the first four i2b2 Challenges as i2b2 NLP Research Data Sets. A similar set of notes from the most recent i2b2 Challenge will be released on the one year anniversary of that Challenge. These data sets have already enabled hundreds of journal and conference articles by the research community.

    ", - "short_description": "Unstructured notes from the Research Patient Data Repository at Partners Healthcare.", - "project_key": "NLP", - "permission_scheme": "PRIVATE", "agreement_forms_required": true, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": false + "is_contest": false, + "visible": true } } ] \ No newline at end of file diff --git a/app/projects/migrations/0013_auto_20180208_2102.py b/app/projects/migrations/0013_auto_20180208_2102.py new file mode 100644 index 00000000..5f5ceca0 --- /dev/null +++ b/app/projects/migrations/0013_auto_20180208_2102.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-08 21:02 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import projects.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0012_auto_20180206_2021'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='project_visibility', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='agreementform', + name='form_html', + field=models.FileField(upload_to=projects.models.get_agreement_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + ), + ] diff --git a/app/projects/migrations/0014_auto_20180208_2107.py b/app/projects/migrations/0014_auto_20180208_2107.py new file mode 100644 index 00000000..4fcd1de5 --- /dev/null +++ b/app/projects/migrations/0014_auto_20180208_2107.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-08 21:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0013_auto_20180208_2102'), + ] + + operations = [ + migrations.RenameField( + model_name='dataproject', + old_name='project_visibility', + new_name='project_visible', + ), + ] diff --git a/app/projects/migrations/0015_auto_20180208_2107.py b/app/projects/migrations/0015_auto_20180208_2107.py new file mode 100644 index 00000000..88eb46b3 --- /dev/null +++ b/app/projects/migrations/0015_auto_20180208_2107.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-08 21:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0014_auto_20180208_2107'), + ] + + operations = [ + migrations.RenameField( + model_name='dataproject', + old_name='project_visible', + new_name='visible', + ), + ] diff --git a/app/projects/migrations/0016_datagate.py b/app/projects/migrations/0016_datagate.py new file mode 100644 index 00000000..ad0e9900 --- /dev/null +++ b/app/projects/migrations/0016_datagate.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-10 22:18 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0015_auto_20180208_2107'), + ] + + operations = [ + migrations.CreateModel( + name='DataGate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data_location_type', models.CharField(choices=[('FILE_SERVICE_URL', 'FileService Signed URL'), ('EXTERNAL_APP_URL', 'External Application URL')], max_length=15)), + ('data_location', models.CharField(max_length=250)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.DataProject')), + ], + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index e99a67a4..31300ed7 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -5,6 +5,16 @@ from django.core.validators import FileExtensionValidator +FILE_SERVICE_URL = 'FILE_SERVICE_URL' +EXTERNAL_APP_URL = 'EXTERNAL_APP_URL' +BLUE = 'BLUE' + +DATA_LOCATION_TYPE = ( + (FILE_SERVICE_URL, 'FileService Signed URL'), + (EXTERNAL_APP_URL, 'External Application URL') +) + + def get_agreement_form_upload_path(instance, filename): form_directory = 'agreementforms/' @@ -63,11 +73,18 @@ class DataProject(models.Model): project_supervisor = models.CharField(max_length=255, blank=True, null=True, verbose_name="Project Supervisor") # TODO change to a choice field and create an enumerable of options (contest, data project) is_contest = models.BooleanField(default=False, blank=False, null=False) + visible = models.BooleanField(default=False, blank=False, null=False) def __str__(self): return '%s %s' % (self.project_key, self.name) +class DataGate(models.Model): + project = models.ForeignKey(DataProject) + data_location_type = models.CharField(max_length=15, choices=DATA_LOCATION_TYPE) + data_location = models.CharField(max_length=250) + + class SignedAgreementForm(models.Model): """ This represents the fully signed agreement form. diff --git a/app/projects/templatetags/projects_extras.py b/app/projects/templatetags/projects_extras.py index c3ac3a50..1abd6de3 100644 --- a/app/projects/templatetags/projects_extras.py +++ b/app/projects/templatetags/projects_extras.py @@ -1,10 +1,11 @@ from django import template from django.conf import settings -from os.path import normpath -from os.path import join + import os import furl +from django.utils.safestring import mark_safe + register = template.Library() @register.filter @@ -31,3 +32,38 @@ def get_login_url(current_uri): login_url.args.add('email_confirm_success_url', current_uri) return login_url.url + +@register.filter +def keyvalue(passed_dictionary, key): + return passed_dictionary[key] + +@register.filter +def keyvalue_permission_scheme(passed_dictionary, key): + return passed_dictionary[key]['permission_scheme'] + +@register.filter +def permission_requested(dictionary, project_key): + return project_key in dictionary + +@register.filter +def is_request_granted(dictionary, project_key): + return dictionary[project_key]['request_granted'] + +@register.filter +def get_date_requested(dictionary, project_key): + return dictionary[project_key]["date_requested"] + +@register.simple_tag +def modal_contact_form_button(text='Contact us', classes='btn btn-primary btn-md'): + + return mark_safe(""" + + """.format(classes, text)) + + +@register.simple_tag +def modal_contact_form_link(text='Contact us', classes=''): + + return mark_safe(""" + {} + """.format(classes, text)) \ No newline at end of file diff --git a/app/projects/views.py b/app/projects/views.py index 5cb8022d..e7bd0ed8 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -21,24 +21,18 @@ from .models import AgreementForm from .models import SignedAgreementForm -from profile.views import user_has_manage_permission from profile.views import get_task_context_data from profile.forms import RegistrationForm from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.urls import reverse -from django.contrib import messages - from django.core.exceptions import ObjectDoesNotExist -from django.template.loader import render_to_string -from django.core.mail import EmailMultiAlternatives -from socket import gaierror +from hypatio.sciauthz_services import SciAuthZ # Get an instance of a logger logger = logging.getLogger(__name__) + @user_auth_and_jwt def signout(request): logout(request) @@ -74,54 +68,26 @@ def save_signed_agreement_form(request): return HttpResponse(200) + @user_auth_and_jwt def submit_user_permission_request(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) data_request = {"user": request.user.email, "item": request.POST['project_key']} - # Send the authorization request to SciAuthZ - create_auth_request_url = settings.AUTHORIZATION_REQUEST_URL - requests.post(create_auth_request_url, headers=jwt_headers, data=json.dumps(data_request), verify=False) + sciauthz.current_user_request_access(data_request) return HttpResponse(200) -# TODO DELETE -@user_auth_and_jwt -def submit_request(request): - - user_jwt = request.COOKIES.get("DBMI_JWT", None) - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} - - # TODO replace with newer models. - - # dua = DataUseAgreement.objects.get(id=request.POST['dua_id']) - - # date_requested = datetime.now().isoformat() - - # # Save the signed DUA - # dua_signed = DataUseAgreementSign(data_use_agreement=dua, - # user=request.user, - # date_signed=date_requested, - # agreement_text=request.POST['agreement_text']) - # dua_signed.save() - - # data_request = {"user": request.user.email, - # "item": request.POST['project_key']} - - # # Send the authorization request to SciAuthZ - # create_auth_request_url = settings.AUTHORIZATION_REQUEST_URL - # requests.post(create_auth_request_url, headers=jwt_headers, data=json.dumps(data_request), verify=False) - - return HttpResponse(200) @public_user_auth_and_jwt def list_data_projects(request, template_name='dataprojects/list.html'): - all_data_projects = DataProject.objects.filter(is_contest=False) + all_data_projects = DataProject.objects.filter(is_contest=False, visible=True) data_projects = [] projects_with_view_permissions = [] @@ -137,19 +103,11 @@ def list_data_projects(request, template_name='dataprojects/list.html'): user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - # If the JWT has expired or the user doesn't have one, force the user to login again - if user_jwt is None or validate_jwt(request) is None: - logout_redirect(request) - - # TODO Does this user have MANAGE permissions on any item? - is_manager = user_has_manage_permission(request, 'n2c2-t1') - - # The JWT token that will get passed in API calls - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, 'n2c2-t1') + user_permissions = sciauthz.current_user_permissions() + user_access_requests = sciauthz.current_user_access_requests() - # Get all of the user's VIEW permissions - permissions_url = settings.USER_PERMISSIONS_URL + "?email=" + user.email - user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() logger.debug('[HYPATIO][DEBUG] User Permissions: ' + json.dumps(user_permissions)) if user_permissions is not None and 'results' in user_permissions: @@ -161,9 +119,8 @@ def list_data_projects(request, template_name='dataprojects/list.html'): # Get all of the user's permission requests access_requests_url = settings.AUTHORIZATION_REQUEST_URL + "?email=" + user.email - logger.debug('[HYPATIO][DEBUG] access_requests_url: ' + access_requests_url) - user_access_requests = requests.get(access_requests_url, headers=jwt_headers, verify=False).json() + logger.debug('[HYPATIO][DEBUG] access_requests_url: ' + access_requests_url) logger.debug('[HYPATIO][DEBUG] User Permission Requests: ' + json.dumps(user_access_requests)) if user_access_requests is not None and 'results' in user_access_requests: @@ -178,6 +135,7 @@ def list_data_projects(request, template_name='dataprojects/list.html'): # Build the dictionary with all project and permission information needed for project in all_data_projects: + project_data_url = None user_has_view_permissions = project.project_key in projects_with_view_permissions if project.project_key in projects_with_access_requests: @@ -187,12 +145,17 @@ def list_data_projects(request, template_name='dataprojects/list.html'): user_requested_access_on = None user_requested_access = False + datagate = project.datagate_set.first() + + if datagate: + project_data_url = datagate.data_location + # Package all the necessary information into one dictionary project = {"name": project.name, "short_description": project.short_description, "description": project.description, "project_key": project.project_key, - # "project_url": project.project_url, + "project_url": project_data_url, "permission_scheme": project.permission_scheme, "user_has_view_permissions": user_has_view_permissions, "user_requested_access": user_requested_access, @@ -224,15 +187,8 @@ def list_data_contests(request, template_name='datacontests/list.html'): user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - # If the JWT has expired or the user doesn't have one, force the user to login again - if user_jwt is None or validate_jwt(request) is None: - logout_redirect(request) - - # TODO Does this user have MANAGE permissions on any item? - is_manager = user_has_manage_permission(request, 'n2c2-t1') - - # The JWT token that will get passed in API calls - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, 'n2c2-t1') # Build the dictionary with all project and permission information needed for data_contest in all_data_contests: @@ -271,12 +227,8 @@ def manage_contests(request, template_name='datacontests/managecontests.html'): user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - # If the JWT has expired or the user doesn't have one, force the user to login again - if user_jwt is None or validate_jwt(request) is None: - logout_redirect(request) - - # Confirm that the user has MANAGE permissions for this item - is_manager = user_has_manage_permission(request, data_contest) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, data_contest) if not is_manager: logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + data_contest + '.') @@ -362,7 +314,8 @@ def grant_access_with_view_permissions(request): logger.debug('[HYPATIO][DEBUG] Granting authorization request ' + authorization_request_id + ' and view permissions to ' + person_email + ' for project ' + project + '.') - is_manager = user_has_manage_permission(request, project) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, project) if not is_manager: logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + project + '.') @@ -427,9 +380,11 @@ def project_details(request, project_key, template_name='project_details.html'): else: user = request.user user_logged_in = True - is_manager = user_has_manage_permission(request, project_key) user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, project_key) + # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} diff --git a/app/templates/dataprojects/access_request.html b/app/templates/dataprojects/access_request.html index b92b362c..50ad5f5a 100644 --- a/app/templates/dataprojects/access_request.html +++ b/app/templates/dataprojects/access_request.html @@ -7,28 +7,30 @@ {% load static %} - + -
    -

    Please select an available Data Use Agreement to sign:

    - {% for agreement_form in agreement_forms %} - - {% endfor %} -
    - - + + + + {% else %} +
    + In order to gain access to this data an administrator will have to contact you with further steps. Click below to confirm your request.

    + + +
    + {% endif %} - - +
    @@ -56,7 +58,7 @@ diff --git a/app/templates/dataprojects/blurb.html b/app/templates/dataprojects/blurb.html index 4529da75..4ecb6a8a 100644 --- a/app/templates/dataprojects/blurb.html +++ b/app/templates/dataprojects/blurb.html @@ -1,3 +1,5 @@ +{% load projects_extras %} +
    - {% else %} - + {% else %} + + + + {% if data_project.permission_scheme == "PUBLIC" %} + + {% elif data_project.permission_scheme == "PRIVATE" %} + {% if data_project.user_has_view_permissions %} + Click here to access the {{ data_project.name }} + {% else %} + {% if data_project.user_requested_access %} + + You've already requested access to this project on {{ data_project.user_requested_access_on }}, please wait for your access to be granted. + +
    +
    + + {% else %} + Request access to the {{ data_project.name }} + {% endif %} + + {% endif %} + {% endif %} + {% endif %}
    From a7b2c712a6ed6db58ac2acc4f77fdfe5c8d25275 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 12 Feb 2018 09:45:10 -0500 Subject: [PATCH 119/613] TC-114: Migration merges --- .../migrations/0017_merge_20180212_1433.py | 16 ++++++++++ docker-compose.override.yml | 32 +++++++++---------- docker-compose.yml | 20 ++++++------ 3 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 app/projects/migrations/0017_merge_20180212_1433.py diff --git a/app/projects/migrations/0017_merge_20180212_1433.py b/app/projects/migrations/0017_merge_20180212_1433.py new file mode 100644 index 00000000..c3a2f2ba --- /dev/null +++ b/app/projects/migrations/0017_merge_20180212_1433.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-12 14:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0016_datagate'), + ('projects', '0013_auto_20180209_1412'), + ] + + operations = [ + ] diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c190a349..cd709e83 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -2,44 +2,44 @@ version: '2.1' services: hypatio: environment: - - AWS_SECRET_ACCESS_KEY - - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=***REMOVED*** + - AWS_ACCESS_KEY_ID=***REMOVED*** - PS_PATH=secret.dbmi.hypatio.DEV build: - context: ~/src/apps/hypatio + context: ~/Documents/virtualenvironments/hypatio-app dockerfile: Dockerfile volumes: - - ~/src/apps/hypatio/app:/app + - ~/Documents/virtualenvironments/hypatio-app/app:/app scireg: environment: - - AWS_SECRET_ACCESS_KEY - - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=***REMOVED*** + - AWS_ACCESS_KEY_ID=***REMOVED*** - PS_PATH=secret.dbmi.scireg.DEV build: - context: ~/src/apps/SciReg + context: ~/Documents/virtualenvironments/scireg/SciReg dockerfile: Dockerfile volumes: - - ~/src/apps/SciReg/app:/app + - ~/Documents/virtualenvironments/scireg/SciReg/app:/app sciauth: environment: - - AWS_SECRET_ACCESS_KEY - - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=***REMOVED*** + - AWS_ACCESS_KEY_ID=***REMOVED*** - PS_PATH=secret.dbmi.sciauth.DEV build: - context: ~/src/apps/SciAuth + context: ~/Documents/virtualenvironments/sciauth/SciAuth dockerfile: Dockerfile volumes: - - ~/src/apps/SciAuth/app:/app + - ~/Documents/virtualenvironments/sciauth/SciAuth/app:/app sciauthz: environment: - - AWS_SECRET_ACCESS_KEY - - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=***REMOVED*** + - AWS_ACCESS_KEY_ID=***REMOVED*** - PS_PATH=secret.dbmi.sciauthz.DEV build: - context: ~/src/apps/SciAuthZ + context: ~/Documents/virtualenvironments/SciAuthZ dockerfile: Dockerfile volumes: - - ~/src/apps/SciAuthZ/app:/app + - ~/Documents/virtualenvironments/SciAuthZ/app:/app networks: portal: driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 4806298e..2a123ed2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,5 @@ version: '2.1' services: - hypatio: - container_name: hypatio - image: dbmi/hypatio - networks: - portal: - aliases: - - portal.aws.dbmi-dev.hms.harvard.edu - ports: - - "80:80" - - "443:443" scireg: container_name: scireg image: dbmi/scireg @@ -37,6 +27,16 @@ services: - authorization.aws.dbmi-dev.hms.harvard.edu ports: - "8003:8003" + hypatio: + container_name: hypatio + image: dbmi/hypatio + networks: + portal: + aliases: + - portal.aws.dbmi-dev.hms.harvard.edu + ports: + - "80:80" + - "443:443" networks: portal: driver: bridge From dc0fc0637c6f4a0cb60dea93e6652168fcc60a24 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 12 Feb 2018 09:52:15 -0500 Subject: [PATCH 120/613] File not supposed to be in git Oops... --- docker-compose.override.yml | 45 ------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 docker-compose.override.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index cd709e83..00000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,45 +0,0 @@ -version: '2.1' -services: - hypatio: - environment: - - AWS_SECRET_ACCESS_KEY=***REMOVED*** - - AWS_ACCESS_KEY_ID=***REMOVED*** - - PS_PATH=secret.dbmi.hypatio.DEV - build: - context: ~/Documents/virtualenvironments/hypatio-app - dockerfile: Dockerfile - volumes: - - ~/Documents/virtualenvironments/hypatio-app/app:/app - scireg: - environment: - - AWS_SECRET_ACCESS_KEY=***REMOVED*** - - AWS_ACCESS_KEY_ID=***REMOVED*** - - PS_PATH=secret.dbmi.scireg.DEV - build: - context: ~/Documents/virtualenvironments/scireg/SciReg - dockerfile: Dockerfile - volumes: - - ~/Documents/virtualenvironments/scireg/SciReg/app:/app - sciauth: - environment: - - AWS_SECRET_ACCESS_KEY=***REMOVED*** - - AWS_ACCESS_KEY_ID=***REMOVED*** - - PS_PATH=secret.dbmi.sciauth.DEV - build: - context: ~/Documents/virtualenvironments/sciauth/SciAuth - dockerfile: Dockerfile - volumes: - - ~/Documents/virtualenvironments/sciauth/SciAuth/app:/app - sciauthz: - environment: - - AWS_SECRET_ACCESS_KEY=***REMOVED*** - - AWS_ACCESS_KEY_ID=***REMOVED*** - - PS_PATH=secret.dbmi.sciauthz.DEV - build: - context: ~/Documents/virtualenvironments/SciAuthZ - dockerfile: Dockerfile - volumes: - - ~/Documents/virtualenvironments/SciAuthZ/app:/app -networks: - portal: - driver: bridge From 5ac9e9be51d48655d7928f162e346595702bf5ff Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 12 Feb 2018 10:20:15 -0500 Subject: [PATCH 121/613] Exclude sqlite3 file. --- .gitignore | 3 ++- app/db.sqlite3 | Bin 165888 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 app/db.sqlite3 diff --git a/.gitignore b/.gitignore index ca576e7a..291970e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ app/assets/* *.pyc */.DS_Store .DS_Store -*.log \ No newline at end of file +*.log +app/db.sqlite3 diff --git a/app/db.sqlite3 b/app/db.sqlite3 deleted file mode 100644 index 8a6940019f5215283091d7442ff344d856ed905f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165888 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 From dc2e436c915237a6073eff23374467028efb6268 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 12 Feb 2018 10:40:46 -0500 Subject: [PATCH 122/613] Ignore docker-compose override file. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 291970e2..447f72e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ app/assets/* .DS_Store *.log app/db.sqlite3 +docker-compose.override.yml From 9aceecbe6c4e398fd56c5607e9db1f158e7296d3 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 12 Feb 2018 10:53:49 -0500 Subject: [PATCH 123/613] Adding original docker-compose.override.yml file back in. And removed from gitignore. --- .gitignore | 1 - docker-compose.override.yml | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docker-compose.override.yml diff --git a/.gitignore b/.gitignore index 447f72e7..291970e2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ app/assets/* .DS_Store *.log app/db.sqlite3 -docker-compose.override.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..640dff52 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,45 @@ +version: '2.1' +services: + hypatio: + environment: + - AWS_SECRET_ACCESS_KEY + - AWS_ACCESS_KEY_ID + - PS_PATH=secret.dbmi.hypatio.DEV + build: + context: ~/src/apps/hypatio + dockerfile: Dockerfile + volumes: + - ~/src/apps/hypatio/app:/app + scireg: + environment: + - AWS_SECRET_ACCESS_KEY + - AWS_ACCESS_KEY_ID + - PS_PATH=secret.dbmi.scireg.DEV + build: + context: ~/src/apps/SciReg + dockerfile: Dockerfile + volumes: + - ~/src/apps/SciReg/app:/app + sciauth: + environment: + - AWS_SECRET_ACCESS_KEY + - AWS_ACCESS_KEY_ID + - PS_PATH=secret.dbmi.sciauth.DEV + build: + context: ~/src/apps/SciAuth + dockerfile: Dockerfile + volumes: + - ~/src/apps/SciAuth/app:/app + sciauthz: + environment: + - AWS_SECRET_ACCESS_KEY + - AWS_ACCESS_KEY_ID + - PS_PATH=secret.dbmi.sciauthz.DEV + build: + context: ~/src/apps/SciAuthZ + dockerfile: Dockerfile + volumes: + - ~/src/apps/SciAuthZ/app:/app +networks: + portal: + driver: bridge \ No newline at end of file From dbeb6e19450740ce261ce56703df2bbbeba2c398 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 13 Feb 2018 15:46:19 -0500 Subject: [PATCH 124/613] TC-110: Adds contest management screen and associated functionality Adds the contest management screen, which displays teams and team info on a modal. Adds a migration for giving agreement forms a short name (displayed on contest management screen). Adds another migration that adds a status field to the Team model. Adds views/URLs for changing team status (finalize, activate, deactivate). Removes some legacy code for granting VIEW permissions since that must now be done at the point of a contest manager activating a team. NOTE: code still needed to grant or revoke VIEW permissions based on team status changes. --- .gitignore | 1 + app/hypatio/scireg_services.py | 6 +- app/hypatio/settings.py | 3 + app/profile/views.py | 11 +- app/projects/admin.py | 2 +- app/projects/fixtures/projects.json | 2 + .../0018_agreementform_short_name.py | 21 ++ .../migrations/0019_auto_20180213_1854.py | 25 ++ app/projects/models.py | 9 +- app/projects/urls.py | 14 +- app/projects/views.py | 139 +++++----- app/projects/views_teams.py | 45 ++++ app/templates/base.html | 9 +- .../datacontests/managecontests.html | 188 +++++++++----- app/templates/datacontests/manageteams.html | 138 ++++++++++ app/templates/datacontests/teamdetails.html | 238 ++++++++++-------- app/templates/profile/profile.html | 2 +- app/templates/project_details.html | 26 -- 18 files changed, 592 insertions(+), 287 deletions(-) create mode 100644 app/projects/migrations/0018_agreementform_short_name.py create mode 100644 app/projects/migrations/0019_auto_20180213_1854.py create mode 100644 app/templates/datacontests/manageteams.html diff --git a/.gitignore b/.gitignore index 291970e2..9c8a348a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ app/assets/* .DS_Store *.log app/db.sqlite3 +app/hypatio/local_settings.py diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index a8f9db90..30771af4 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -11,7 +11,7 @@ def build_headers_with_jwt(user_jwt): def send_confirmation_email(user_jwt, current_uri): - send_confirm_email_url = settings.SCIREG_SERVER_URL + '/api/register/send_confirmation_email/' + send_confirm_email_url = settings.SCIREG_REGISTRATION_URL + '/send_confirmation_email/' logger.debug("[P2M2][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) @@ -24,9 +24,7 @@ def send_confirmation_email(user_jwt, current_uri): def check_email_confirmation(user_jwt): - check_email_confirm_url = settings.SCIREG_SERVER_URL + '/api/register/' - - response = requests.get(check_email_confirm_url, headers=build_headers_with_jwt(user_jwt)) + response = requests.get(settings.SCIREG_REGISTRATION_URL, headers=build_headers_with_jwt(user_jwt)) try: email_status = response.json()['results'][0]['email_confirmed'] diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 8196b6e5..4a06f52f 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -46,6 +46,7 @@ 'jquery', 'bootstrap3', 'contact', + 'django_countries', 'profile', 'projects', 'pyauth0jwt', @@ -132,6 +133,8 @@ AUTHORIZATION_REQUEST_URL = AUTHZ_BASE + "/authorization_requests/" AUTHORIZATION_REQUEST_GRANT_URL = AUTHZ_BASE + "/authorization_request_change/" +SCIREG_REGISTRATION_URL = SCIREG_SERVER_URL + "/api/register/" + COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN") SSL_SETTING = "https" diff --git a/app/profile/views.py b/app/profile/views.py index 26b3f221..e793dd5b 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -43,17 +43,15 @@ def update_profile(request): # Create a new registration with a POST if registration_form.cleaned_data['id'] == "": - registration_url = settings.SCIREG_SERVER_URL + '/api/register/' - requests.post(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=False) + requests.post(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=False) # Update an existing registration with a PUT to the specific ID else: - registration_url = settings.SCIREG_SERVER_URL + '/api/register/' + registration_form.cleaned_data['id'] + '/' + registration_url = settings.SCIREG_REGISTRATION_URL + registration_form.cleaned_data['id'] + '/' requests.put(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=False) return HttpResponse(200) else: - logger.debug('[HYPATIO][DEBUG] Profile form errors: ' + form.errors.as_json()) - + # logger.debug('[HYPATIO][DEBUG] Profile form errors: ' + form.errors.as_json()) # TODO Not implemented return HttpResponse(status=500) @@ -71,8 +69,7 @@ def profile(request, template_name='profile/profile.html'): jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} # Query SciReg to get the user's information - registration_url = settings.SCIREG_SERVER_URL + '/api/register/' - registration_info = requests.get(registration_url, headers=jwt_headers, verify=False).json() + registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() logger.debug('[HYPATIO][DEBUG] Registration info ' + json.dumps(registration_info)) diff --git a/app/projects/admin.py b/app/projects/admin.py index 5ecdec12..fea43eb6 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -11,7 +11,7 @@ class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') #, 'project_url') class AgreementformAdmin(admin.ModelAdmin): - list_display = ('name', 'form_html') + list_display = ('name', 'short_name', 'form_html') class SignedagreementformAdmin(admin.ModelAdmin): list_display = ('user', 'agreement_form', 'date_signed') diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index 6ec1777c..39f5aeef 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -12,6 +12,7 @@ "pk": 1, "fields": { "name": "N2C2 Data Use Agreement", + "short_name": "DUA", "created": "2018-01-01T00:00:00+00:00", "form_html": "agreementforms/c757e275-486f-4c3d-ab52-e295ccb8f45a.html" } @@ -21,6 +22,7 @@ "pk": 2, "fields": { "name": "N2C2 Rules of Conduct", + "short_name": "ROC", "created": "2018-01-01T00:00:00+00:00", "form_html": "agreementforms/e3b33c4d-94d2-4738-9d4c-308548943435.html" } diff --git a/app/projects/migrations/0018_agreementform_short_name.py b/app/projects/migrations/0018_agreementform_short_name.py new file mode 100644 index 00000000..98c7e2ce --- /dev/null +++ b/app/projects/migrations/0018_agreementform_short_name.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-13 17:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0017_merge_20180212_1433'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='short_name', + field=models.CharField(default='X', max_length=6, verbose_name='short_name'), + preserve_default=False, + ), + ] diff --git a/app/projects/migrations/0019_auto_20180213_1854.py b/app/projects/migrations/0019_auto_20180213_1854.py new file mode 100644 index 00000000..19b1d2b5 --- /dev/null +++ b/app/projects/migrations/0019_auto_20180213_1854.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-13 18:54 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0018_agreementform_short_name'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Ready', 'Ready to be activated'), ('Active', 'Activated'), ('Deactivated', 'Deactivated')], default='Pending', max_length=30), + ), + migrations.AlterField( + model_name='agreementform', + name='short_name', + field=models.CharField(max_length=6), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 4ffcaaec..400a99a5 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -14,6 +14,12 @@ (EXTERNAL_APP_URL, 'External Application URL') ) +TEAM_STATUS = ( + ('Pending', 'Pending'), + ('Ready', 'Ready to be activated'), + ('Active', 'Activated'), + ('Deactivated', 'Deactivated') +) def get_agreement_form_upload_path(instance, filename): @@ -47,8 +53,8 @@ class AgreementForm(models.Model): This represents the type of forms that a user might need to sign to be granted access to a data set, such as a data use agreement or rules of conduct. The form file should be an html file. """ - # TODO add form validation to check for html goodness name = models.CharField(max_length=100, blank=False, null=False, verbose_name="name") + short_name = models.CharField(max_length=6, blank=False, null=False) created = models.DateTimeField(auto_now_add=True) form_html = models.FileField(upload_to=get_agreement_form_upload_path, validators=[FileExtensionValidator(allowed_extensions=['html'])]) @@ -99,6 +105,7 @@ class SignedAgreementForm(models.Model): class Team(models.Model): team_leader = models.OneToOneField(User) data_project = models.ForeignKey(DataProject) + status = models.CharField(max_length=30, choices=TEAM_STATUS, default='Pending') def __str__(self): return '%s' % self.team_leader.email diff --git a/app/projects/urls.py b/app/projects/urls.py index e5c82fcb..f0860736 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -4,16 +4,19 @@ from .views import list_data_contests from .views import request_access from .views import submit_user_permission_request -from .views import manage_contests +from .views import manage_contest from .views import save_signed_agreement_form -from .views import grant_access_with_view_permissions from .views import project_details from .views import signout +from .views import view_team_management from .views_teams import join_team from .views_teams import create_team_from_pi from .views_teams import team_signup_form from .views_teams import approve_team_join from .views_teams import reject_team_join +from .views_teams import finalize_team +from .views_teams import activate_team +from .views_teams import deactivate_team urlpatterns = ( url(r'^$', list_data_projects), @@ -21,14 +24,17 @@ url(r'^list_data_contests/$', list_data_contests), url(r'^request_access/$', request_access), url(r'^submit_user_permission_request/$', submit_user_permission_request), - url(r'^managecontests/$', manage_contests), - url(r'^grantviewpermissions', grant_access_with_view_permissions), + url(r'^manage/(?P[^/]+)/$', manage_contest), + url(r'^view_team_management/$', view_team_management), url(r'^save_signed_agreement_form', save_signed_agreement_form), url(r'^signout/$', signout), url(r'^join_team/$', join_team), url(r'^approve_team_join/$', approve_team_join), url(r'^reject_team_join/$', reject_team_join), url(r'^create_team_from_pi/$', create_team_from_pi), + url(r'^finalize_team/$', finalize_team), + url(r'^activate_team/$', activate_team), + url(r'^deactivate_team/$', deactivate_team), url(r'^team_signup_form/(P[^/]+)/$', team_signup_form), url(r'^(?P[^/]+)/$', project_details) ) diff --git a/app/projects/views.py b/app/projects/views.py index 3971e498..ba994f55 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib.auth import logout +from django.db.models import Count from django.shortcuts import render from django.shortcuts import redirect from django.shortcuts import get_object_or_404 @@ -68,7 +69,6 @@ def save_signed_agreement_form(request): return HttpResponse(200) - @user_auth_and_jwt def submit_user_permission_request(request): @@ -83,7 +83,6 @@ def submit_user_permission_request(request): return HttpResponse(200) - @public_user_auth_and_jwt def list_data_projects(request, template_name='dataprojects/list.html'): @@ -212,93 +211,89 @@ def list_data_contests(request, template_name='datacontests/list.html'): "profile_server_url": settings.SCIREG_SERVER_URL}) @user_auth_and_jwt -def manage_contests(request, template_name='datacontests/managecontests.html'): - - all_data_contests = DataProject.objects.filter(is_contest=True) - data_contests = [] +def view_team_management(request, template_name='datacontests/manageteams.html'): + """ + Populates the team management modal popup on the contest management screen. + """ - # TODO eventually this shouldn't be hard coded for n2c2 - data_contest = "n2c2-t1" + project_key = request.GET["project"] + team_leader = request.GET["team"] - # This dictionary will hold all user requests and permissions - user_details = {} + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(data_project=project, team_leader__email=team_leader) + num_required_forms = project.agreement_forms.count() - user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission(request, data_contest) - - if not is_manager: - logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + data_contest + '.') - return HttpResponse(403) - - # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} - authorization_request_url = settings.AUTHORIZATION_REQUEST_URL + "?item=" + data_contest - logger.debug('[HYPATIO][DEBUG] authorization_request_url: ' + authorization_request_url) - - # Query SciAuthZ for all access requests to this contest - authorization_requests = requests.get(authorization_request_url, headers=jwt_headers, verify=False).json() - logger.debug('[HYPATIO][DEBUG] Item Permission Requests: ' + json.dumps(authorization_requests)) + # Collect all the team member information needed + team_members = [] - if authorization_requests is not None and 'results' in authorization_requests: - authorization_requests_json = authorization_requests['results'] + for member in team.participant_set.all(): + email = member.user.email - for authorization_request in authorization_requests_json: - - date_requested = "" - date_request_granted = "" + # Make a request to SciReg for a specific person's user information + user_info_json = requests.get(settings.SCIREG_REGISTRATION_URL + "?email=" + email, headers=jwt_headers, verify=False).json() + if user_info_json['count'] != 0: + user_info = user_info_json["results"][0] + else: + user_info = None + + signed_agreement_forms = SignedAgreementForm.objects.filter(user__email=email, project=project) - if authorization_request['date_requested'] is not None: - date_requested = datetime.strftime(datetime.strptime(authorization_request['date_requested'], "%Y-%m-%dT%H:%M:%S"), "%b %d %Y, %H:%M:%S") + team_members.append({ + 'email': email, + 'user_info': user_info, + 'signed_agreement_forms': signed_agreement_forms, + 'participant': member + }) - if authorization_request['date_request_granted'] is not None: - date_request_granted = datetime.strftime(datetime.strptime(authorization_request['date_request_granted'], "%Y-%m-%dT%H:%M:%S"), "%b %d %Y, %H:%M:%S") + return render(request, template_name, context={"project": project, + "team": team, + "team_members": team_members, + "num_required_forms": num_required_forms}) - # Store the permission request in a dictionary and include a blank permissions key to be added later - user_detail = {'personal_information': {'first_name': '', - 'last_name': ''}, - 'authorization_request': {'date_requested': date_requested, - 'request_granted': authorization_request['request_granted'], - 'date_request_granted': date_request_granted, - 'request_id': authorization_request['id']}, - 'permissions': []} +@user_auth_and_jwt +def manage_contest(request, project_key, template_name='datacontests/managecontests.html'): - # Add a key to the user details dictionary with the user email as the key and permis - user_details[authorization_request['user']] = user_detail + project = DataProject.objects.get(project_key=project_key) - # Query SciAuthZ for all permissions to this contest - permissions_url = settings.USER_PERMISSIONS_URL + "?item=" + data_contest - user_permissions = requests.get(permissions_url, headers=jwt_headers, verify=False).json() - logger.debug('[HYPATIO][DEBUG] User Permissions: ' + json.dumps(user_permissions)) + # This dictionary will hold all user requests and permissions + user_details = {} - if user_permissions is not None and 'results' in user_permissions: - user_permissions = user_permissions["results"] + user = request.user + user_logged_in = True + user_jwt = request.COOKIES.get("DBMI_JWT", None) - for permission in user_permissions: + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, project_key) - # If this user is not already in the user detail dictionary, add them (user does not have a permission request apparently) - if permission['user'] not in user_details: - user_details[permission['user']] = {'personal_information': {'first_name': '', - 'last_name': ''}, - 'authorization_request': {}, - 'permissions': []} + if not is_manager: + logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) - # Add this permission to the user details dictionary - user_details[permission['user']]['permissions'].append(permission['permission']) + teams = Team.objects.filter(data_project=project) - logger.debug('[HYPATIO][DEBUG] user_details: ' + json.dumps(user_details)) + # Simple statistics for display + total_teams = teams.count() + total_participants = Participant.objects.filter(data_challenge=project).count() + countries_represented = '?' # TODO + total_submissions = 0 # TODO + teams_with_any_submission = 0 # TODO return render(request, template_name, {"user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, - "user_details": user_details, - "project": data_contest}) - + "project": project, + "teams": teams, + "total_teams": total_teams, + "total_participants": total_participants, + "countries_represented": countries_represented, + "total_submissions": total_submissions, + "teams_with_any_submission": teams_with_any_submission}) + +# TODO remove this: activate_team view should now do this @user_auth_and_jwt def grant_access_with_view_permissions(request): @@ -368,7 +363,7 @@ def project_details(request, project_key, template_name='project_details.html'): team = None team_members = None team_has_pending_members = None - user_is_pi = False + user_is_team_leader = False institution = project.institution current_step = None @@ -392,8 +387,7 @@ def project_details(request, project_key, template_name='project_details.html'): get_task_context_data(request) # Make a request to SciReg to grab email verification and profile information - profile_registration_url = settings.SCIREG_SERVER_URL + '/api/register/' - profile_registration_info = requests.get(profile_registration_url, headers=jwt_headers, verify=False).json() + profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() if profile_registration_info['count'] != 0: profile_registration_info = profile_registration_info["results"][0] @@ -452,7 +446,7 @@ def project_details(request, project_key, template_name='project_details.html'): if participant and team: team_members = Participant.objects.filter(team=team) team_has_pending_members = team_members.filter(team_approved=False) - user_is_pi = team.team_leader == request.user + user_is_team_leader = team.team_leader == request.user try: all_teams = Team.objects.filter(data_project__project_key=project_key) @@ -481,7 +475,7 @@ def project_details(request, project_key, template_name='project_details.html'): "participant": participant, "all_teams": all_teams, "team": team, - "user_is_pi": user_is_pi, + "user_is_team_leader": user_is_team_leader, "team_members": team_members, "team_has_pending_members": team_has_pending_members, "access_granted": access_granted, @@ -495,6 +489,3 @@ def project_details(request, project_key, template_name='project_details.html'): "registration_form": registration_form, "current_step": current_step}) - - - diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 1ae4ff29..dfc3f8c3 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -14,6 +14,51 @@ logger = logging.getLogger(__name__) +@user_auth_and_jwt +def deactivate_team(request): + project_key = request.POST.get("project") + team = request.POST.get("team") + + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(team_leader__email=team, data_project=project) + + team.status = 'Deactivated' + team.save() + + # TODO: Need to revoke VIEW permissions to all team members + # ... + + return HttpResponse(200) + +@user_auth_and_jwt +def activate_team(request): + project_key = request.POST.get("project") + team = request.POST.get("team") + + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(team_leader__email=team, data_project=project) + + team.status = 'Active' + team.save() + + # TODO: Need to grant VIEW permissions to all team members + # ... + + return HttpResponse(200) + +@user_auth_and_jwt +def finalize_team(request): + project_key = request.POST.get("project_key") + team = request.POST.get("team") + + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(team_leader__email=team, data_project=project) + + team.status = 'Ready' + team.save() + + return HttpResponse(200) + @user_auth_and_jwt def approve_team_join(request): project_key = request.POST.get("project_key") diff --git a/app/templates/base.html b/app/templates/base.html index db93bca2..8c1ece95 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -12,7 +12,7 @@ integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"> - + {% load bootstrap3 %} {% bootstrap_css %} {% bootstrap_javascript %} @@ -165,11 +165,12 @@
  • Profile
  • {% if is_manager %} - -
  • Manage Projects
  • + + {# TODO remove the project_key eventually #} -
  • Manage Contests
  • +
  • Manage Contests
  • {% endif %} diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index ab485a2a..0b0c3ad3 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -1,82 +1,142 @@ {% extends 'base.html' %} {% block headscripts %} + + + + {% endblock %} -{% block title %}Manage Data Challenges{% endblock %} -{% block subtitle %}Temporary page to manage N2C2{% endblock %} +{% block title %}{{ project.project_key }} Management{% endblock %} +{% block subtitle %}{{ project.short_description }}{% endblock %} -{% block content %} -
    +{% block content %} +
    +
    -
    Manage Permissions
    - - - - - - - - - - - - - - - {% for key, value in user_details.items %} - - - - - - - - - - - {% endfor %} - -
    EmailFirst NameLast NameDUA SignedCC SignedDate RequestedPermissionsActions
    {{ key }}first namelast nameyes/noyes/no{{ value.authorization_request.date_requested }} - {% if value.authorization_request.request_granted == False %} - None - {% else %} - {{ value.permissions|join:", " }} - {% endif %} - - {% if value.authorization_request.request_granted == False %} - - {% else %} - - {% endif %} -
    + +
    + + + + + + + + + + {% for team in teams %} + + + + + {% elif team.status == "Active" %} + Active + {% elif team.status == "Deactivated" %} + Deactivated + {% endif %} + + {% endfor %} + +
    TeamMembersStatus
    {{ team }}{{ team.participant_set.all.count }} + {% if team.status == "Pending" %} + Pending + {% elif team.status == "Ready" %} + Ready to Activate
    +
    +
    +
    + +
    +
      +
    • + {{ total_teams }} + Teams +
    • +
    • + {{ total_participants }} + Total Participants +
    • +
    • + {{ countries_represented }} + Countries Represented +
    • +
    • + {{ total_submissions }} + Total Submissions +
    • +
    • + {{ teams_with_any_submission }} + Teams With Submissions +
    • +
    +
    +
    +
    +
    + + {% endblock %} {% block footerscripts %} + + {% endblock %} \ No newline at end of file diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html new file mode 100644 index 00000000..0673da13 --- /dev/null +++ b/app/templates/datacontests/manageteams.html @@ -0,0 +1,138 @@ +{% load countries %} + +
    +
    +

    Team Leader: {{ team.team_leader.email }}

    +

    Team Status: {{ team.status }}

    +
    +
    +
    + {% if team.status == "Ready" %} + + {% elif team.status == "Active" %} + + {% elif team.status == "Deactivated" %} + + {% endif %} +
    +
    +
    + +
    +
    + + + + + + + + + + + + + {% for member in team_members %} + + + + + + + + + {% endfor %} + +
    NameLocationEmailStatusSigned FormsActions
    {{ member.user_info.first_name }} {{ member.user_info.last_name }} + {% get_country member.user_info.country as country %} + {% if country == 'US' and member.user_info.state != "" %} + {{ member.user_info.state }}, USA + {% else %} + {{ country.name }} + {% endif %} + + + {{ member.email }} +
    + +
    +
    + {% if member.email == team.team_leader.email %} + Team leader + {% elif member.signed_agreement_forms.count != num_required_forms %} + Pending forms + {% elif member.participant.team_pending %} + Pending team leader approval + {% else %} + Active + {% endif %} + + {% for form in member.signed_agreement_forms %} + + {% endfor %} + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/app/templates/datacontests/teamdetails.html b/app/templates/datacontests/teamdetails.html index 5d170ff2..1a4344a4 100644 --- a/app/templates/datacontests/teamdetails.html +++ b/app/templates/datacontests/teamdetails.html @@ -1,10 +1,17 @@ -{% if participant.team_wait_on_leader %} - No action needed at this time. We are waiting for your team leader {{ participant.team_wait_on_leader_email }} to create a team. -{% elif participant.team_pending %} - No action needed at this time. We are waiting for your team leader {{ team.team_leader }} to approve your request to join the team. -{% elif participant.team_approved %} - {% if user_is_pi %} - {% if team_has_pending_members %} +{% if user_is_team_leader %} + {% if team.status == "Active" %} + + {% elif team.status == "Ready" %} + + {% elif team.status == "Deactivated" %} + + {% elif team_has_pending_members %} @@ -12,109 +19,137 @@ - {% endif %} - - - - - - - - - - {% for participant in team_members %} - - - - - {% endfor %} - -
    EmailMembership
    {{ participant.user.email }} - {% if participant.team_approved %} - Approved - {% elif participant.team_pending %} - - - - {% else %} - Error - {% endif %} -
    - - {% if not pi_team_has_pending_members %} - - {% endif %} - {% else %} + {% endif %} + + + + + + + + + + {% for participant in team_members %} + + + + + {% endfor %} + +
    EmailMembership
    {{ participant.user.email }} + {% if participant.team_approved %} + Approved + {% elif participant.team_pending %} + + + + {% else %} + Error + {% endif %} +
    + + {% if team.status == "Pending" and not team_has_pending_members %} + + {% endif %} +{% else %} + {% if participant.team_wait_on_leader %} + No action needed at this time. We are waiting for your team leader {{ participant.team_wait_on_leader_email }} to create a team. + {% elif participant.team_pending %} + No action needed at this time. We are waiting for your team leader {{ team.team_leader }} to approve your request to join the team. + {% elif participant.team_approved %} - {% endif %} -{% else %} - - - - {% csrf_token %} -
    - - + {% else %} + - Don't see your team leader? -

    They might not have registered yet. Enter their e-mail address below and you'll be added to the team when they register.

    - - - -
    - -
    - - - -
    - -

    Are you leading a team?

    - - - - + + + +
    + +

    Are you leading a team?

    + + + + + {% endif %} {% endif %} +{% block footerscripts %} + + \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/app/templates/profile/profile.html b/app/templates/profile/profile.html index f6d68798..3df66711 100644 --- a/app/templates/profile/profile.html +++ b/app/templates/profile/profile.html @@ -45,7 +45,7 @@
    Data Contests
    - Contests here + Coming soon
    diff --git a/app/templates/project_details.html b/app/templates/project_details.html index ff6b7b51..e7f246c6 100644 --- a/app/templates/project_details.html +++ b/app/templates/project_details.html @@ -135,32 +135,6 @@

    {% endif %}

    {% endif %} - -
    {% else %} From efa94922fafc3c88d81b80b4051387334aa29b53 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 14 Feb 2018 12:40:23 -0500 Subject: [PATCH 125/613] TC-130: Remove PI drop down and force user to enter an email address Users now must enter the team leader's email address and wait to be granted access. Users can now also reset their team status back to empty if they entered the email incorrectly. --- app/projects/urls.py | 6 +- app/projects/views.py | 7 -- app/projects/views_teams.py | 41 +++++--- .../datacontests/managecontests.html | 3 +- app/templates/datacontests/teamdetails.html | 95 ++++++++++--------- app/templates/project_details.html | 4 +- 6 files changed, 85 insertions(+), 71 deletions(-) diff --git a/app/projects/urls.py b/app/projects/urls.py index f0860736..312764d7 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -10,7 +10,8 @@ from .views import signout from .views import view_team_management from .views_teams import join_team -from .views_teams import create_team_from_pi +from .views_teams import leave_team +from .views_teams import create_team from .views_teams import team_signup_form from .views_teams import approve_team_join from .views_teams import reject_team_join @@ -29,9 +30,10 @@ url(r'^save_signed_agreement_form', save_signed_agreement_form), url(r'^signout/$', signout), url(r'^join_team/$', join_team), + url(r'^leave_team/$', leave_team), url(r'^approve_team_join/$', approve_team_join), url(r'^reject_team_join/$', reject_team_join), - url(r'^create_team_from_pi/$', create_team_from_pi), + url(r'^create_team/$', create_team), url(r'^finalize_team/$', finalize_team), url(r'^activate_team/$', activate_team), url(r'^deactivate_team/$', deactivate_team), diff --git a/app/projects/views.py b/app/projects/views.py index ba994f55..d43f7416 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -354,7 +354,6 @@ def project_details(request, project_key, template_name='project_details.html'): registration_form = None agreement_forms_list = [] participant = None - all_teams = None is_manager = False user_requested_access = False user_access_request_granted = False @@ -448,11 +447,6 @@ def project_details(request, project_key, template_name='project_details.html'): team_has_pending_members = team_members.filter(team_approved=False) user_is_team_leader = team.team_leader == request.user - try: - all_teams = Team.objects.filter(data_project__project_key=project_key) - except ObjectDoesNotExist: - all_teams = None - # If all other steps completed, then last step will be team if current_step is None: current_step = "team" @@ -473,7 +467,6 @@ def project_details(request, project_key, template_name='project_details.html'): return render(request, template_name, {"project": project, "agreement_forms_list": agreement_forms_list, "participant": participant, - "all_teams": all_teams, "team": team, "user_is_team_leader": user_is_team_leader, "team_members": team_members, diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index dfc3f8c3..232a840d 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -101,36 +101,49 @@ def reject_team_join(request): return HttpResponse(200) +@user_auth_and_jwt +def leave_team(request): + project_key = request.POST.get("project_key") + project = DataProject.objects.get(project_key=project_key) + + team_leader = request.POST.get("team_leader") + + participant = Participant.objects.get(user=request.user) + participant.team = None + participant.pending = False + participant.approved = False + participant.team_wait_on_leader_email = None + participant.team_wait_on_leader = False + participant.save() + + return redirect('/projects/' + request.POST.get('project_key') + '/') + @user_auth_and_jwt def join_team(request): project_key = request.POST.get("project_key") project = DataProject.objects.get(project_key=project_key) - existing_pi_team_to_join = request.POST.get("existing_pi_team_to_join") - pi_to_wait_for = request.POST.get("pi_to_wait_for") + team_leader = request.POST.get("team_leader") try: participant = Participant.objects.get(user=request.user) except ObjectDoesNotExist: participant = create_participant(request.user, project) - if pi_to_wait_for != "": - participant.team_wait_on_leader_email = pi_to_wait_for - participant.team_wait_on_leader = True - participant.save() - elif existing_pi_team_to_join != "": - try: - team = Team.objects.get(team_leader__email=existing_pi_team_to_join) - except ObjectDoesNotExist: - team = None - + try: + # If this team leader has already created a team, add the person to the team in a pending status + team = Team.objects.get(team_leader__email=team_leader) participant.team = team participant.team_pending = True participant.save() + except ObjectDoesNotExist: + # If this team leader has not yet created a team, mark the person as waiting + participant.team_wait_on_leader_email = team_leader + participant.team_wait_on_leader = True + participant.save() return redirect('/projects/' + request.POST.get('project_key') + '/') - # TODO What is this used for? @user_auth_and_jwt def team_signup_form(request, project_key): @@ -150,7 +163,7 @@ def team_signup_form(request, project_key): @user_auth_and_jwt -def create_team_from_pi(request): +def create_team(request): """Creates a new team with the given user as its team leader. """ diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index 0b0c3ad3..8fdf1a1f 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -35,12 +35,13 @@

    Team List

    {% if team.status == "Pending" %} Pending {% elif team.status == "Ready" %} - Ready to Activate + Ready to Activate {% elif team.status == "Active" %} Active {% elif team.status == "Deactivated" %} Deactivated {% endif %} + {% endfor %} diff --git a/app/templates/datacontests/teamdetails.html b/app/templates/datacontests/teamdetails.html index 1a4344a4..1e17684c 100644 --- a/app/templates/datacontests/teamdetails.html +++ b/app/templates/datacontests/teamdetails.html @@ -59,7 +59,16 @@ {% endif %} {% else %} {% if participant.team_wait_on_leader %} - No action needed at this time. We are waiting for your team leader {{ participant.team_wait_on_leader_email }} to create a team. +

    No action needed at this time. We are waiting for your team leader {{ participant.team_wait_on_leader_email }} to create a team and approve your access.

    +
    +

    Is your team leader using a different email address? Click the button below to return to the team join step.

    + +
    + {% csrf_token %} + + +
    + {% elif participant.team_pending %} No action needed at this time. We are waiting for your team leader {{ team.team_leader }} to approve your request to join the team. {% elif participant.team_approved %} @@ -68,60 +77,56 @@
    {% else %} -
    - {% csrf_token %} -
    - - -
    - - Don't see your team leader? -

    They might not have registered yet. Enter their e-mail address below and you'll be added to the team when they register.

    - - - -
    - -
    - -
    - -
    - -

    Are you leading a team?

    - - +
    +
    +

    Joining a team?

    - - - \ No newline at end of file +
    \ No newline at end of file From 17fea36757623f13079573981138da4ee6f51347 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 1 Mar 2018 10:57:04 -0500 Subject: [PATCH 160/613] TC-44: Updates disclaimer logic, hard coded for now --- app/templates/project_compete.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/project_compete.html b/app/templates/project_compete.html index adc2c01f..b77b2ec9 100644 --- a/app/templates/project_compete.html +++ b/app/templates/project_compete.html @@ -17,9 +17,9 @@

    {{ file.description }}

    -

    Disclaimer here... Something about not sending this to other people and that you are agreeing to compete in this contest if you download. Your download is logged.

    +

    Reminder: Gaining access to any portion of the 2018 n2c2 data commits your team to participate in the evaluation that will be run by n2c2, to submit a paper describing your developed system, and to present your work in the follow-up workshop to be organized by n2c2 (should your paper be accepted for presentation). Teams cannot withdraw after gaining access to the data. (See Rules of Conduct and Data Use Agreement).

    - Download Data + Download Data
    {% endif %}

    From 463923227e3f35a7baf9d209960ddd3e6507bf18 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 1 Mar 2018 11:09:14 -0500 Subject: [PATCH 161/613] TC-44: Spacing things a little better --- app/templates/project_compete.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/templates/project_compete.html b/app/templates/project_compete.html index b77b2ec9..a011a590 100644 --- a/app/templates/project_compete.html +++ b/app/templates/project_compete.html @@ -17,8 +17,12 @@

    {{ file.description }}

    +
    +

    Reminder: Gaining access to any portion of the 2018 n2c2 data commits your team to participate in the evaluation that will be run by n2c2, to submit a paper describing your developed system, and to present your work in the follow-up workshop to be organized by n2c2 (should your paper be accepted for presentation). Teams cannot withdraw after gaining access to the data. (See Rules of Conduct and Data Use Agreement).

    +
    + Download Data
    {% endif %} From 0061579107e2208f672730f9e8ce7543dff8eae2 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Sat, 3 Mar 2018 22:37:05 -0500 Subject: [PATCH 162/613] TC-161 - Allow admins to see files that aren't enabled for everyone yet. --- app/projects/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/projects/views.py b/app/projects/views.py index a22ac6cd..81b06fc6 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -525,9 +525,14 @@ def project_details(request, project_key, template_name='project_details.html'): access_granted = True # TODO Temporarily ordering by name descending for n2c2 + # Get all of the files available for this data set - data_files = HostedFile.objects.filter(project=project).order_by('-long_name') + if request.user.is_superuser: + data_files = HostedFile.objects.filter(project=project).order_by('-long_name') + else: + data_files = HostedFile.objects.filter(project=project, enabled=True).order_by('-long_name') + # If all other steps completed, then last step will be team if current_step is None: current_step = "team" From 7ea0ebb066de562366b09b704f3b732c347e2039 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Sat, 3 Mar 2018 23:06:28 -0500 Subject: [PATCH 163/613] TC-161 - Showing files for superuser. --- app/templates/project_compete.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/project_compete.html b/app/templates/project_compete.html index a011a590..53186b82 100644 --- a/app/templates/project_compete.html +++ b/app/templates/project_compete.html @@ -13,7 +13,7 @@

    - {% if file.enabled %} + {% if file.enabled or user.is_superuser %}

    {{ file.description }}

    From 6c2d98af4b73d8523bb234e708bbd529a7dbf853 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Sun, 4 Mar 2018 14:35:14 -0500 Subject: [PATCH 164/613] TC-161 - Adding ability to lock down registration. --- .../0026_dataproject_registration_open.py | 20 +++++++++++++++++++ app/projects/models.py | 1 + app/templates/project_compete.html | 8 ++++---- .../project_registration_closed.html | 5 +++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 app/projects/migrations/0026_dataproject_registration_open.py create mode 100644 app/templates/project_registration_closed.html diff --git a/app/projects/migrations/0026_dataproject_registration_open.py b/app/projects/migrations/0026_dataproject_registration_open.py new file mode 100644 index 00000000..81c3148d --- /dev/null +++ b/app/projects/migrations/0026_dataproject_registration_open.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-04 03:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0025_auto_20180227_1835'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='registration_open', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 00d18306..ae7edb45 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -81,6 +81,7 @@ class DataProject(models.Model): # TODO change to a choice field and create an enumerable of options (contest, data project) is_contest = models.BooleanField(default=False, blank=False, null=False) visible = models.BooleanField(default=False, blank=False, null=False) + registration_open = models.BooleanField(default=False, blank=False, null=False) def __str__(self): return '%s %s' % (self.project_key, self.name) diff --git a/app/templates/project_compete.html b/app/templates/project_compete.html index 53186b82..ca9e777a 100644 --- a/app/templates/project_compete.html +++ b/app/templates/project_compete.html @@ -2,6 +2,10 @@ Your team has been approved for access to this challenge. In the sections below, you will be able to download training and test data sets and upload your solutions.
    + +
    {% for file in data_files %} @@ -19,10 +23,6 @@


    -

    Reminder: Gaining access to any portion of the 2018 n2c2 data commits your team to participate in the evaluation that will be run by n2c2, to submit a paper describing your developed system, and to present your work in the follow-up workshop to be organized by n2c2 (should your paper be accepted for presentation). Teams cannot withdraw after gaining access to the data. (See Rules of Conduct and Data Use Agreement).

    - -
    - Download Data

    {% endif %} diff --git a/app/templates/project_registration_closed.html b/app/templates/project_registration_closed.html new file mode 100644 index 00000000..71c7c931 --- /dev/null +++ b/app/templates/project_registration_closed.html @@ -0,0 +1,5 @@ + + + From f30e8e0081f298fe6e18895d049174e98185cd5c Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Sun, 4 Mar 2018 14:59:38 -0500 Subject: [PATCH 165/613] TC-161 - I can't logic. --- app/templates/project_details.html | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/templates/project_details.html b/app/templates/project_details.html index aaf37d09..fe671a66 100644 --- a/app/templates/project_details.html +++ b/app/templates/project_details.html @@ -31,13 +31,23 @@

    Description
    - {% if not user_logged_in %} - {% include 'project_login.html' %} - {% elif not access_granted %} - {% include 'project_signup.html' %} - {% elif access_granted %} - {% include 'project_compete.html' %} - {% endif %} + + {% if not user_logged_in %} + {% if project.registration_open or user.is_superuser %} + {% include 'project_login.html' %} + {% else %} + {% include 'project_registration_closed.html' %} + {% endif %} + {% elif not access_granted %} + {% if project.registration_open or user.is_superuser %} + {% include 'project_signup.html' %} + {% else %} + {% include 'project_registration_closed.html' %} + {% endif %} + {% elif access_granted %} + {% include 'project_compete.html' %} + {% endif %} +
    {% endblock %} From f11ac88f086522f258e16542c79ec72d033d6f5f Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 7 Mar 2018 16:28:39 -0500 Subject: [PATCH 166/613] TC-177, TC-178, TC-157: Allows contest managers to change a team's status or delete a team And team members will be notified by email accordingly. --- app/projects/models.py | 2 +- app/projects/urls.py | 8 +- app/projects/views_files.py | 2 - app/projects/views_teams.py | 108 ++++++++++++++---- .../datacontests/managecontests.html | 2 +- app/templates/datacontests/manageteams.html | 105 +++++++++++------ .../email_new_team_status_notification.html | 24 ++++ .../email_new_team_status_notification.txt | 7 ++ .../email_team_deleted_notification.html | 21 ++++ .../email/email_team_deleted_notification.txt | 8 ++ 10 files changed, 222 insertions(+), 65 deletions(-) create mode 100644 app/templates/email/email_new_team_status_notification.html create mode 100644 app/templates/email/email_new_team_status_notification.txt create mode 100644 app/templates/email/email_team_deleted_notification.html create mode 100644 app/templates/email/email_team_deleted_notification.txt diff --git a/app/projects/models.py b/app/projects/models.py index ae7edb45..654bdafd 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -116,7 +116,7 @@ def __str__(self): class Participant(models.Model): user = models.OneToOneField(User) data_challenge = models.ForeignKey(DataProject) - team = models.ForeignKey(Team, null=True, blank=True) + team = models.ForeignKey(Team, null=True, blank=True, on_delete=models.CASCADE) team_wait_on_leader_email = models.CharField(max_length=100, blank=True, null=True) team_wait_on_leader = models.BooleanField(default=False) team_pending = models.BooleanField(default=False) diff --git a/app/projects/urls.py b/app/projects/urls.py index 346b59bc..35126098 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -17,8 +17,8 @@ from .views_teams import approve_team_join from .views_teams import reject_team_join from .views_teams import finalize_team -from .views_teams import activate_team -from .views_teams import deactivate_team +from .views_teams import change_team_status +from .views_teams import delete_team from .views_files import download_dataset @@ -38,8 +38,8 @@ url(r'^reject_team_join/$', reject_team_join), url(r'^create_team/$', create_team), url(r'^finalize_team/$', finalize_team), - url(r'^activate_team/$', activate_team), - url(r'^deactivate_team/$', deactivate_team), + url(r'^change_team_status/$', change_team_status), + url(r'^delete_team/$', delete_team), url(r'^team_signup_form/(P[^/]+)/$', team_signup_form), url(r'^download_signed_form/$', download_signed_form), url(r'^download_dataset/$', download_dataset), diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 780d74a9..cd102c02 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -37,8 +37,6 @@ def download_dataset(request): # Save a record of this person downloading this file HostedFileDownload.objects.create(user=request.user, hosted_file=file_to_download) - s3 = boto3.resource('s3') - s3_filename = file_to_download.file_location + "/" + file_to_download.file_name logger.debug("[views_files][download_dataset] - User " + request.user.email + " is downloading file " + s3_filename + " from bucket " + settings.S3_BUCKET + ".") diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index ba81111c..fdd4e887 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -12,6 +12,7 @@ from .models import DataProject from .models import Participant from .models import Team +from .models import SignedAgreementForm from contact.views import email_send @@ -20,42 +21,109 @@ logger = logging.getLogger(__name__) @user_auth_and_jwt -def deactivate_team(request): +def change_team_status(request): + """Change a team's status, assign the correct permissions, and notify team members.""" + project_key = request.POST.get("project") - team = request.POST.get("team") + team_leader = request.POST.get("team") + status = request.POST.get("status") - logger.debug('[HYPATIO][deactivate_team] ' + request.user.email + ' deactivating team ' + team + ' for project ' + project_key + '.') + logger.debug('[HYPATIO][change_team_status] ' + request.user.email + ' changing team ' + team_leader + ' for project ' + project_key + ' to status of ' + status + '.') project = DataProject.objects.get(project_key=project_key) - team = Team.objects.get(team_leader__email=team, data_project=project) + team = Team.objects.get(team_leader__email=team_leader, data_project=project) + + # First change the team's status + if status == "pending": + team.status = "Pending" + team.save() + elif status == "ready": + team.status = "Ready" + team.save() + elif status == "active": + team.status = "Active" + team.save() + elif status == "deactivated": + team.status = "Deactivated" + team.save() + else: + logger.debug('[HYPATIO][change_team_status] Given status "' + status + '" not one of allowed statuses.') + return HttpResponse(500) + + logger.debug('[HYPATIO][change_team_status] Adjusting VIEW permissions for team members.') + + # If a status is anything other than active, remove VIEW permissions + if status in ["active"]: + for member in team.participant_set.all(): + sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz.create_view_permission(project_key, member.user.email) + else: + for member in team.participant_set.all(): + sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz.remove_view_permission(project_key, member.user.email) + + logger.debug('[HYPATIO][change_team_status] Emailing a notification to team members.') + + # Send an email notification to the team + context = {'status': status, + 'project': project, + 'site_url': settings.SITE_URL} - team.status = 'Deactivated' - team.save() + # Email list + emails = [member.user.email for member in team.participant_set.all()] - # Grant VIEW permissions to each person on this team - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) - for member in team.participant_set.all(): - sciauthz.remove_view_permission(project_key, member.user.email) + email_success = email_send(subject='DBMI Portal Team Status Changed', + recipients=emails, + email_template='email_new_team_status_notification', + extra=context) return HttpResponse(200) @user_auth_and_jwt -def activate_team(request): +def delete_team(request): + """Delete a team and notify members.""" + project_key = request.POST.get("project") - team = request.POST.get("team") + team_leader = request.POST.get("team") + administrator_message = request.POST.get("administrator_message") - logger.debug('[HYPATIO][activate_team] ' + request.user.email + ' activating team ' + team + ' for project ' + project_key + '.') + logger.debug('[HYPATIO][delete_team] ' + request.user.email + ' is deleting team ' + team_leader + ' for project ' + project_key + '.') project = DataProject.objects.get(project_key=project_key) - team = Team.objects.get(team_leader__email=team, data_project=project) + team = Team.objects.get(team_leader__email=team_leader, data_project=project) - team.status = 'Active' - team.save() + logger.debug('[HYPATIO][delete_team] Removing all VIEW permissions for team members.') - # Grant VIEW permissions to each person on this team - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + # First revoke all VIEW permissions for member in team.participant_set.all(): - sciauthz.create_view_permission(project_key, member.user.email) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz.remove_view_permission(project_key, member.user.email) + + logger.debug('[HYPATIO][delete_team] Deleting all signed forms by team members.') + + for member in team.participant_set.all(): + SignedAgreementForm.objects.filter(user__email=member.user.email, project=project).delete() + + logger.debug('[HYPATIO][delete_team] Sending a notification to team members.') + + # Then send a notification to the team members + context = {'administrator_message': administrator_message, + 'project': project, + 'site_url': settings.SITE_URL} + + emails = [member.user.email for member in team.participant_set.all()] + + email_success = email_send(subject='DBMI Portal Team Deleted', + recipients=emails, + email_template='email_team_deleted_notification', + extra=context) + + logger.debug('[HYPATIO][delete_team] Deleting the team from the database.') + + # Then delete the team + team.delete() + + logger.debug('[HYPATIO][delete_team] Team ' + team_leader + ' for project ' + project_key + ' successfully deleted.') return HttpResponse(200) @@ -206,7 +274,6 @@ def team_signup_form(request, project_key): return render(request, "teams/manageteam.html", {"participant": participant, "teams": teams}) - @user_auth_and_jwt def create_team(request): """Creates a new team with the given user as its team leader. @@ -233,7 +300,6 @@ def create_team(request): return redirect('/projects/' + project_key + '/') - def create_participant(user, project): """ Creates a participant object and returns it. """ diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index 0ac3a023..d55f6f9b 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -36,7 +36,7 @@

    Team List

    {% if team.status == "Pending" %} Pending {% elif team.status == "Ready" %} - Ready to Activate + Ready to Activate {% elif team.status == "Active" %} Active {% elif team.status == "Deactivated" %} diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index f5ebcb9d..0d05c765 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -1,26 +1,47 @@ {% load countries %}
    -
    +

    Team Leader: {{ team.team_leader.email }}

    -

    Team Status: {{ team.status }}

    -
    +
    - {% if team.status == "Ready" %} - - {% elif team.status == "Active" %} - - {% elif team.status == "Deactivated" %} - - {% endif %} + +
    +
    +
    + +
    - +
    @@ -53,7 +74,7 @@ {% elif member.participant.team_pending %} Pending team leader approval {% else %} - Active + No issues {% endif %} {% for team in teams %} - + + @@ -154,6 +155,7 @@

    Challenge Submissions

    {% for upload in uploads %} + ").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter); +else if(b.length>0){p.nTFoot=b[0];da(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)", +"g"),Wa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,L=function(a){return!a||!0===a||"-"===a?!0:!1},Nb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Ob=function(a,b){Xa[b]||(Xa[b]=RegExp(Pa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Xa[b],"."):a},Ya=function(a,b,c){var d="string"===typeof a;if(L(a))return!0;b&&d&&(a=Ob(a,b));c&&d&&(a=a.replace(Wa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Pb=function(a,b,c){return L(a)?!0:!(L(a)||"string"=== +typeof a)?null:Ya(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb=/<.*?>/g,Na=m.util.throttle,Rb=[],w=Array.prototype,ac=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof +h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=aa(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&& +c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Tb(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Tb(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){bb(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Ub=function(a,b,c,d,e){for(var c= +[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Ub(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc):"";if(k)switch(k[2]){case "visIdx":case "visible":b= +parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[Z(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()", +"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Ub,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()", +function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ia(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ia(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m= +h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;id;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot? +h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};m.camelToHungarian=I;o("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments); +a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){na(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a=this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a|| +!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j)); +b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c, +1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,n){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,n)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=Q(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.16";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow= +{nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null, +sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g, +this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+ +"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries", +sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"}; +X(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null, +bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[], +aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null, +aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength, +b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}}, +order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd", +sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead", +sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Kb=m.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ha(a, +b)]},simple_numbers:function(a,b){return["previous",ha(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ha(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ha(a,b),"last"]},_numbers:ha,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},n,l,m=0,o=function(b,d){var k,s,u,r,v=function(b){Sa(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{n=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":n=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":n=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":n=j.sNext;l=r+(e",{"class":g.sPageButton+ +" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(n).appendTo(b);Va(u,{action:r},v);m++}}}},s;try{s=h(b).find(G.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+s+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a); +return null!==b&&!isNaN(b)||L(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c,!0)?"html-num-fmt"+c:null},function(a){return L(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return L(a)?a:"string"===typeof a?a.replace(Mb," ").replace(Aa,""):""},string:function(a){return L(a)? +a:"string"===typeof a?a.replace(Mb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Ob(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return L(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return L(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a< +b?-1:a>b?1:0},"string-desc":function(a,b){return ab?-1:0}});cb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b); +h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Vb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g, +"""):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Vb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Vb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Lb,_fnBuildAjax:sa,_fnAjaxUpdate:kb,_fnAjaxParameters:tb,_fnAjaxUpdateDraw:ub, +_fnAjaxDataSrc:ta,_fnAddColumn:Da,_fnColumnOptions:ja,_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:Z,_fnColumnIndexToVisible:$,_fnVisbleColumns:aa,_fnGetColumns:la,_fnColumnTypes:Fa,_fnApplyColumnDefs:hb,_fnHungarianMap:X,_fnCamelToHungarian:I,_fnLanguageCompat:Ca,_fnBrowserDetect:fb,_fnAddData:M,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:ib, +_fnSplitObjNotation:Ia,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:R,_fnGetDataMaster:Ja,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ca,_fnGetRowElements:Ha,_fnCreateTr:Ga,_fnBuildHead:jb,_fnDrawHead:ea,_fnDraw:N,_fnReDraw:S,_fnAddOptionsHtml:mb,_fnDetectHeader:da,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:ob,_fnFilterComplete:fa,_fnFilterCustom:xb,_fnFilterColumn:wb,_fnFilter:vb,_fnFilterCreateSearch:Oa,_fnEscapeRegex:Pa,_fnFilterData:yb,_fnFeatureHtmlInfo:rb,_fnUpdateInfo:Bb,_fnInfoMacros:Cb,_fnInitialise:ga, +_fnInitComplete:ua,_fnLengthChange:Qa,_fnFeatureHtmlLength:nb,_fnFeatureHtmlPaginate:sb,_fnPageChange:Sa,_fnFeatureHtmlProcessing:pb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:qb,_fnScrollDraw:ka,_fnApplyToChildren:H,_fnCalculateColumnWidths:Ea,_fnThrottle:Na,_fnConvertToWidth:Db,_fnGetWidestNode:Eb,_fnGetMaxLenString:Fb,_fnStringToCss:v,_fnSortFlatten:V,_fnSort:lb,_fnSortAria:Hb,_fnSortListener:Ua,_fnSortAttachListener:La,_fnSortingClasses:wa,_fnSortData:Gb,_fnSaveState:xa,_fnLoadState:Ib,_fnSettingsFromNode:ya, +_fnLog:J,_fnMap:F,_fnBindAction:Va,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Ra,_fnRenderer:Ma,_fnDataSource:y,_fnRowAttributes:Ka,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); \ No newline at end of file diff --git a/app/static/plugins/intercooler/intercooler.min.js b/app/static/plugins/intercooler/intercooler.min.js new file mode 100644 index 00000000..b1e4601f --- /dev/null +++ b/app/static/plugins/intercooler/intercooler.min.js @@ -0,0 +1,2 @@ +/*! intercooler 1.2.1 2017-12-14 */ +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.Intercooler=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):a.Intercooler=b(a.jQuery)}(this,function($){var Intercooler=Intercooler||function(){"use strict";function remove(a){a.remove()}function showIndicator(a){a.closest(".ic-use-transition").length>0?(a.data("ic-use-transition",!0),a.removeClass("ic-use-transition")):a.show()}function hideIndicator(a){a.data("ic-use-transition")?(a.data("ic-use-transition",null),a.addClass("ic-use-transition")):a.hide()}function fixICAttributeName(a){return USE_DATA?"data-"+a:a}function getICAttribute(a,b){return a.attr(fixICAttributeName(b))}function setICAttribute(a,b,c){a.attr(fixICAttributeName(b),c)}function prepend(a,b){try{a.prepend(b)}catch(b){log(a,formatError(b),"ERROR")}if(getICAttribute(a,"ic-limit-children")){var c=parseInt(getICAttribute(a,"ic-limit-children"));a.children().length>c&&a.children().slice(c,a.children().length).remove()}}function append(a,b){try{a.append(b)}catch(b){log(a,formatError(b),"ERROR")}if(getICAttribute(a,"ic-limit-children")){var c=parseInt(getICAttribute(a,"ic-limit-children"));a.children().length>c&&a.children().slice(0,a.children().length-c).remove()}}function triggerEvent(a,b,c){$.zepto&&(b=b.split(".").reverse().join(":")),a.trigger(b,c)}function log(a,b,c){if(null==a&&(a=$("body")),triggerEvent(a,"log.ic",[b,c,a]),"ERROR"==c){window.console&&window.console.log("Intercooler Error : "+b);var d=closestAttrValue($("body"),"ic-post-errors-to");d&&$.post(d,{error:b})}}function uuid(){return _UUID++}function icSelectorFor(a){return getICAttributeSelector("ic-id='"+getIntercoolerId(a)+"'")}function parseInterval(a){return log(null,"POLL: Parsing interval string "+a,"DEBUG"),"null"==a||"false"==a||""==a?null:a.lastIndexOf("ms")==a.length-2?parseFloat(a.substr(0,a.length-2)):a.lastIndexOf("s")==a.length-1?1e3*parseFloat(a.substr(0,a.length-1)):1e3}function getICAttributeSelector(a){return"["+fixICAttributeName(a)+"]"}function initScrollHandler(){null==_scrollHandler&&(_scrollHandler=function(){$(getICAttributeSelector("ic-trigger-on='scrolled-into-view'")).each(function(){var a=$(this);isScrolledIntoView(getTriggeredElement(a))&&1!=a.data("ic-scrolled-into-view-loaded")&&(a.data("ic-scrolled-into-view-loaded",!0),fireICRequest(a))})},$(window).scroll(_scrollHandler))}function currentUrl(){return window.location.pathname+window.location.search+window.location.hash}function createDocument(a){var b=null;return/<(html|body)/i.test(a)?(b=document.documentElement.cloneNode(),b.innerHTML=a):(b=document.documentElement.cloneNode(!0),b.querySelector("body").innerHTML=a),$(b)}function getTarget(a){return getTargetImpl(a,"ic-target")}function getTargetImpl(a,b){var c=$(a).closest(getICAttributeSelector(b)),d=getICAttribute(c,b);return"this"==d?c:d&&0!=d.indexOf("this.")?0==d.indexOf("closest ")?a.closest(d.substr(8)):0==d.indexOf("find ")?a.find(d.substr(5)):$(d):a}function processHeaders(a,b){a=$(a),triggerEvent(a,"beforeHeaders.ic",[a,b]),log(a,"response headers: "+b.getAllResponseHeaders(),"DEBUG");var c=null;if(b.getResponseHeader("X-IC-Title")&&(document.title=b.getResponseHeader("X-IC-Title")),b.getResponseHeader("X-IC-Refresh")){var d=b.getResponseHeader("X-IC-Refresh").split(",");log(a,"X-IC-Refresh: refreshing "+d,"DEBUG"),$.each(d,function(b,c){refreshDependencies(c.replace(/ /g,""),a)})}if(b.getResponseHeader("X-IC-Script")&&(log(a,"X-IC-Script: evaling "+b.getResponseHeader("X-IC-Script"),"DEBUG"),globalEval(b.getResponseHeader("X-IC-Script"),[["elt",a]])),b.getResponseHeader("X-IC-Redirect")&&(log(a,"X-IC-Redirect: redirecting to "+b.getResponseHeader("X-IC-Redirect"),"DEBUG"),window.location=b.getResponseHeader("X-IC-Redirect")),"true"==b.getResponseHeader("X-IC-CancelPolling")&&cancelPolling(a.closest(getICAttributeSelector("ic-poll"))),"true"==b.getResponseHeader("X-IC-ResumePolling")){var e=a.closest(getICAttributeSelector("ic-poll"));setICAttribute(e,"ic-pause-polling",null),startPolling(e)}if(b.getResponseHeader("X-IC-SetPollInterval")){var e=a.closest(getICAttributeSelector("ic-poll"));cancelPolling(e),setICAttribute(e,"ic-poll",b.getResponseHeader("X-IC-SetPollInterval")),startPolling(e)}b.getResponseHeader("X-IC-Open")&&(log(a,"X-IC-Open: opening "+b.getResponseHeader("X-IC-Open"),"DEBUG"),window.open(b.getResponseHeader("X-IC-Open")));var f=b.getResponseHeader("X-IC-Trigger");if(f)if(log(a,"X-IC-Trigger: found trigger "+f,"DEBUG"),c=getTarget(a),b.getResponseHeader("X-IC-Trigger-Data")){var g=$.parseJSON(b.getResponseHeader("X-IC-Trigger-Data"));triggerEvent(c,f,g)}else f.indexOf("{")>=0?$.each($.parseJSON(f),function(a,b){triggerEvent(c,a,b)}):triggerEvent(c,f,[]);var h=b.getResponseHeader("X-IC-Set-Local-Vars");if(h&&$.each($.parseJSON(h),function(a,b){localStorage.setItem(a,b)}),b.getResponseHeader("X-IC-Remove")&&a){var i=b.getResponseHeader("X-IC-Remove");i+="";var j=parseInterval(i);log(a,"X-IC-Remove header found.","DEBUG"),c=getTarget(a),"true"==i||null==j?remove(c):(c.addClass("ic-removing"),setTimeout(function(){remove(c)},j))}return triggerEvent(a,"afterHeaders.ic",[a,b]),!0}function beforeRequest(a){a.addClass("disabled"),a.data("ic-request-in-flight",!0)}function requestCleanup(a,b){a.length>0&&hideIndicator(a),b.removeClass("disabled"),b.data("ic-request-in-flight",!1),b.data("ic-next-request")&&(b.data("ic-next-request").req(),b.data("ic-next-request",null))}function replaceOrAddMethod(a,b){if("string"===$.type(a)){var c=/(&|^)_method=[^&]*/,d="&_method="+b;return c.test(a)?a.replace(c,d):a+d}return a.append("_method",b),a}function isIdentifier(a){return/^[$A-Z_][0-9A-Z_$]*$/i.test(a)}function globalEval(a,b){var c=[],d=[];if(b)for(var e=0;e0?getICAttribute(c,b):null}function formatError(a){var b=a.toString()+"\n";try{b+=a.stack}catch(a){}return b}function handleRemoteRequest(a,b,c,d,e){beforeRequest(a),d=replaceOrAddMethod(d,b);var f=findIndicator(a);f.length>0&&showIndicator(f);var g,h=uuid(),i=new Date;g=USE_ACTUAL_HTTP_METHOD?b:"GET"==b?"GET":"POST";var j={type:g,url:c,data:d,dataType:"text",headers:{Accept:"text/html-partial, */*; q=0.9","X-IC-Request":!0,"X-HTTP-Method-Override":b},beforeSend:function(e,f){triggerEvent(a,"beforeSend.ic",[a,d,f,e,h]),log(a,"before AJAX request "+h+": "+b+" to "+c,"DEBUG");var g=closestAttrValue(a,"ic-on-beforeSend");g&&globalEval(g,[["elt",a],["data",d],["settings",f],["xhr",e]]),maybeInvokeLocalAction(a,"-beforeSend")},success:function(b,c,d){triggerEvent(a,"success.ic",[a,b,c,d,h]),log(a,"AJAX request "+h+" was successful.","DEBUG");var g=closestAttrValue(a,"ic-on-success");if(!g||0!=globalEval(g,[["elt",a],["data",b],["textStatus",c],["xhr",d]])){var i=new Date;try{if(processHeaders(a,d)){log(a,"Processed headers for request "+h+" in "+(new Date-i)+"ms","DEBUG");var j=new Date;if(d.getResponseHeader("X-IC-PushURL")||"true"==closestAttrValue(a,"ic-push-url"))try{requestCleanup(f,a);var k=d.getResponseHeader("X-IC-PushURL")||closestAttrValue(a,"ic-src");if(!_history)throw"History support not enabled";_history.snapshotForHistory(k)}catch(b){log(a,"Error during history snapshot for "+h+": "+formatError(b),"ERROR")}e(b,c,a,d),log(a,"Process content for request "+h+" in "+(new Date-j)+"ms","DEBUG")}triggerEvent(a,"after.success.ic",[a,b,c,d,h]),maybeInvokeLocalAction(a,"-success")}catch(b){log(a,"Error processing successful request "+h+" : "+formatError(b),"ERROR")}}},error:function(b,d,e){triggerEvent(a,"error.ic",[a,d,e,b]);var f=closestAttrValue(a,"ic-on-error");f&&globalEval(f,[["elt",a],["status",d],["str",e],["xhr",b]]),processHeaders(a,b),maybeInvokeLocalAction(a,"-error"),log(a,"AJAX request "+h+" to "+c+" experienced an error: "+e,"ERROR")},complete:function(b,c){log(a,"AJAX request "+h+" completed in "+(new Date-i)+"ms","DEBUG"),requestCleanup(f,a);try{$.contains(document,a[0])?triggerEvent(a,"complete.ic",[a,d,c,b,h]):triggerEvent($("body"),"complete.ic",[a,d,c,b,h])}catch(b){log(a,"Error during complete.ic event for "+h+" : "+formatError(b),"ERROR")}var e=closestAttrValue(a,"ic-on-complete");e&&globalEval(e,[["elt",a],["xhr",b],["status",c]]),maybeInvokeLocalAction(a,"-complete")}};"string"!=$.type(d)&&(j.dataType=null,j.processData=!1,j.contentType=!1),triggerEvent($(document),"beforeAjaxSend.ic",[j,a]),j.cancel?requestCleanup(f,a):$.ajax(j)}function findIndicator(a){var b=null;if(a=$(a),getICAttribute(a,"ic-indicator"))b=$(getICAttribute(a,"ic-indicator")).first();else if(b=a.find(".ic-indicator").first(),0==b.length){var c=closestAttrValue(a,"ic-indicator");c?b=$(c).first():a.next().is(".ic-indicator")&&(b=a.next())}return b}function processIncludes(a,b){if(0==$.trim(b).indexOf("{")){var c=$.parseJSON(b);$.each(c,function(b,c){a=appendData(a,b,c)})}else $(b).each(function(){var b=$(this).serializeArray();$.each(b,function(b,c){a=appendData(a,c.name,c.value)})});return a}function processLocalVars(a,b){return $(b.split(",")).each(function(){var b=$.trim(this),c=localStorage.getItem(b);c&&(a=appendData(a,b,c))}),a}function appendData(a,b,c){return"string"===$.type(a)?("string"!==$.type(c)&&(c=JSON.stringify(c)),a+"&"+b+"="+encodeURIComponent(c)):(a.append(b,c),a)}function getParametersForElement(a,b,c){var d=getTarget(b),e=null;if(b.is("form")&&"multipart/form-data"==b.attr("enctype"))e=new FormData(b[0]),e=appendData(e,"ic-request",!0);else{e="ic-request=true";var f=b.closest("form");if(b.is("form")||"GET"!=a&&f.length>0){e+="&"+f.serialize();var g=b.data("ic-last-clicked-button");g&&(e=appendData(e,g.name,g.value))}else e+="&"+b.serialize()}var h=closestAttrValue(b,"ic-prompt");if(h){var i=prompt(h);if(!i)return null;var j=closestAttrValue(b,"ic-prompt-name")||"ic-prompt-value";e=appendData(e,j,i)}b.attr("id")&&(e=appendData(e,"ic-element-id",b.attr("id"))),b.attr("name")&&(e=appendData(e,"ic-element-name",b.attr("name"))),getICAttribute(d,"ic-id")&&(e=appendData(e,"ic-id",getICAttribute(d,"ic-id"))),d.attr("id")&&(e=appendData(e,"ic-target-id",d.attr("id"))),c&&c.attr("id")&&(e=appendData(e,"ic-trigger-id",c.attr("id"))),c&&c.attr("name")&&(e=appendData(e,"ic-trigger-name",c.attr("name")));var k=closestAttrValue(b,"ic-include");k&&(e=processIncludes(e,k));var l=closestAttrValue(b,"ic-local-vars");l&&(e=processLocalVars(e,l)),$(getICAttributeSelector("ic-global-include")).each(function(){e=processIncludes(e,getICAttribute($(this),"ic-global-include"))}),e=appendData(e,"ic-current-url",currentUrl());var m=closestAttrValue(b,"ic-select-from-response");return m&&(e=appendData(e,"ic-select-from-response",m)),log(b,"request parameters "+e,"DEBUG"),e}function maybeSetIntercoolerInfo(a){var b=getTarget(a);getIntercoolerId(b),1!=a.data("elementAdded.ic")&&(a.data("elementAdded.ic",!0),triggerEvent(a,"elementAdded.ic"))}function getIntercoolerId(a){return getICAttribute(a,"ic-id")||setICAttribute(a,"ic-id",uuid()),getICAttribute(a,"ic-id")}function processNodes(a){a=$(a),a.length>1?a.each(function(){processNodes(this)}):(processMacros(a),processSources(a),processPolling(a),processEventSources(a),processTriggerOn(a),processRemoveAfter(a),processAddClasses(a),processRemoveClasses(a))}function fireReadyStuff(a){triggerEvent(a,"nodesProcessed.ic"),$.each(_readyHandlers,function(b,c){try{c(a)}catch(b){log(a,formatError(b),"ERROR")}})}function autoFocus(a){a.find("[autofocus]").last().focus()}function processMacros(a){$.each(_MACROS,function(b,c){0==a.closest(".ic-ignore").length&&(a.is("["+c+"]")&&processMacro(c,a),a.find("["+c+"]").each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&processMacro(c,a)}))})}function processSources(a){0==a.closest(".ic-ignore").length&&(a.is(getICAttributeSelector("ic-src"))&&maybeSetIntercoolerInfo(a),a.find(getICAttributeSelector("ic-src")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&maybeSetIntercoolerInfo(a)}))}function processPolling(a){0==a.closest(".ic-ignore").length&&(a.is(getICAttributeSelector("ic-poll"))&&(maybeSetIntercoolerInfo(a),startPolling(a)),a.find(getICAttributeSelector("ic-poll")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&(maybeSetIntercoolerInfo(a),startPolling(a))}))}function processTriggerOn(a){0==a.closest(".ic-ignore").length&&(handleTriggerOn(a),a.find(getICAttributeSelector("ic-trigger-on")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&handleTriggerOn(a)}))}function processRemoveAfter(a){0==a.closest(".ic-ignore").length&&(handleRemoveAfter(a),a.find(getICAttributeSelector("ic-remove-after")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&handleRemoveAfter(a)}))}function processAddClasses(a){0==a.closest(".ic-ignore").length&&(handleAddClasses(a),a.find(getICAttributeSelector("ic-add-class")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&handleAddClasses(a)}))}function processRemoveClasses(a){0==a.closest(".ic-ignore").length&&(handleRemoveClasses(a),a.find(getICAttributeSelector("ic-remove-class")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&handleRemoveClasses(a)}))}function processEventSources(a){0==a.closest(".ic-ignore").length&&(handleEventSource(a),a.find(getICAttributeSelector("ic-sse-src")).each(function(){var a=$(this);0==a.closest(".ic-ignore").length&&handleEventSource(a)}))}function startPolling(a){if(null==a.data("ic-poll-interval-id")&&"true"!=getICAttribute(a,"ic-pause-polling")){var b=parseInterval(getICAttribute(a,"ic-poll"));if(null!=b){var c=icSelectorFor(a),d=parseInt(getICAttribute(a,"ic-poll-repeats"))||-1,e=0;log(a,"POLL: Starting poll for element "+c,"DEBUG");var f=setInterval(function(){var b=$(c);triggerEvent(a,"onPoll.ic",b),0==b.length||e==d||a.data("ic-poll-interval-id")!=f?(log(a,"POLL: Clearing poll for element "+c,"DEBUG"),clearTimeout(f)):fireICRequest(b),e++},b);a.data("ic-poll-interval-id",f)}}}function cancelPolling(a){null!=a.data("ic-poll-interval-id")&&(clearTimeout(a.data("ic-poll-interval-id")),a.data("ic-poll-interval-id",null))}function refreshDependencies(a,b){log(b,"refreshing dependencies for path "+a,"DEBUG"),$(getICAttributeSelector("ic-src")).each(function(){var c=!1,d=$(this);"GET"==verbFor(d)&&"ignore"!=getICAttribute(d,"ic-deps")&&(isDependent(a,getICAttribute(d,"ic-src"))?null!=b&&$(b)[0]==d[0]||(fireICRequest(d),c=!0):(isICDepsDependent(a,getICAttribute(d,"ic-deps"))||"*"==getICAttribute(d,"ic-deps"))&&(null!=b&&$(b)[0]==d[0]||(fireICRequest(d),c=!0))),c&&log(d,"depends on path "+a+", refreshing...","DEBUG")})}function isICDepsDependent(a,b){if(b)for(var c=b.split(","),d=0;d0){var f=a.split(":");d=f[0],e=parseInterval(f[1])}else d=a;setTimeout(function(){b[c](d)},e)}function handleAddClasses(a){if(a=$(a),getICAttribute(a,"ic-add-class"))for(var b=getICAttribute(a,"ic-add-class").split(","),c=b.length,d=0;d=b&&d<=c&&e<=c&&d>=b}function maybeScrollToTarget(a,b){if("false"!=closestAttrValue(a,"ic-scroll-to-target")&&("true"==closestAttrValue(a,"ic-scroll-to-target")||"true"==closestAttrValue(b,"ic-scroll-to-target"))){var c=-50;closestAttrValue(a,"ic-scroll-offset")?c=parseInt(closestAttrValue(a,"ic-scroll-offset")):closestAttrValue(b,"ic-scroll-offset")&&(c=parseInt(closestAttrValue(b,"ic-scroll-offset")));var d=b.offset().top,e=$(window).scrollTop(),f=e+window.innerHeight;(df)&&(c+=d,$("html,body").animate({scrollTop:c},400))}}function getTransitionDuration(a,b){var c=closestAttrValue(a,"ic-transition-duration");if(c)return parseInterval(c);if(c=closestAttrValue(b,"ic-transition-duration"))return parseInterval(c);b=$(b);var d=0,e=b.css("transition-duration");e&&(d+=parseInterval(e));var f=b.css("transition-delay");return f&&(d+=parseInterval(f)),d}function closeSSESource(a){var b=a.data("ic-event-sse-source");try{b&&b.close()}catch(b){log(a,"Error closing ServerSentEvent source"+b,"ERROR")}}function beforeSwapCleanup(a){a.find(getICAttributeSelector("ic-sse-src")).each(function(){closeSSESource($(this))}),triggerEvent(a,"beforeSwap.ic")}function processICResponse(a,b,c,d){if(a&&""!=a&&" "!=a){log(b,"response content: \n"+a,"DEBUG");var e=getTarget(b),f=closestAttrValue(b,"ic-transform-response");f&&(a=globalEval(f,[["content",a],["url",d],["elt",b]]));var g=maybeFilter(a,closestAttrValue(b,"ic-select-from-response"));"true"==closestAttrValue(b,"ic-fix-ids")&&fixIDs(g);var h=function(){if("true"==closestAttrValue(b,"ic-replace-target")){try{beforeSwapCleanup(e),closeSSESource(e),e.replaceWith(g),e=g}catch(a){log(b,formatError(a),"ERROR")}processNodes(g),fireReadyStuff(e),autoFocus(e)}else{if("prepend"==getICAttribute(b,"ic-swap-style"))prepend(e,g),processNodes(g),fireReadyStuff(e),autoFocus(e);else if("append"==getICAttribute(b,"ic-swap-style"))append(e,g),processNodes(g),fireReadyStuff(e),autoFocus(e);else{try{beforeSwapCleanup(e),e.empty().append(g)}catch(a){log(b,formatError(a),"ERROR")}e.children().each(function(){processNodes(this)}),fireReadyStuff(e),autoFocus(e)}1!=c&&maybeScrollToTarget(b,e)}};if(0==e.length)return void log(b,"Invalid target for element: "+getICAttribute(b.closest(getICAttributeSelector("ic-target")),"ic-target"),"ERROR");var i=getTransitionDuration(b,e);e.addClass("ic-transitioning"),setTimeout(function(){try{h()}catch(a){log(b,"Error during content swap : "+formatError(a),"ERROR")}setTimeout(function(){try{e.removeClass("ic-transitioning"),_history&&_history.updateHistory(),triggerEvent(e,"complete_transition.ic",[e])}catch(a){log(b,"Error during transition complete : "+formatError(a),"ERROR")}},20)},i)}else log(b,"Empty response, nothing to do here.","DEBUG")}function maybeFilter(a,b){var c;if($.zepto){var d=createDocument(a);c=$(d).find("body").contents()}else c=$($.parseHTML(a,null,!0));return b?walkTree(c,b).contents():c}function walkTree(a,b){return a.filter(b).add(a.find(b))}function fixIDs(a){var b={};walkTree(a,"[id]").each(function(){var a,c=$(this).attr("id");do a="ic-fixed-id-"+uuid();while($("#"+a).length>0);b[c]=a,$(this).attr("id",a)}),walkTree(a,"label[for]").each(function(){var a=$(this).attr("for");$(this).attr("for",b[a]||a)}),walkTree(a,"*").each(function(){$.each(this.attributes,function(){this.value.indexOf("#")!==-1&&(this.value=this.value.replace(/#([-_A-Za-z0-9]+)/g,function(a,c){return"#"+(b[c]||c)}))})})}function getStyleTarget(a){var b=closestAttrValue(a,"ic-target");return b&&0==b.indexOf("this.style.")?b.substr(11):null}function getAttrTarget(a){var b=closestAttrValue(a,"ic-target");return b&&0==b.indexOf("this.")?b.substr(5):null}function fireICRequest(a,b){a=$(a);var c=a;a.is(getICAttributeSelector("ic-src"))||void 0!=getICAttribute(a,"ic-action")||(a=a.closest(getICAttributeSelector("ic-src")));var d=closestAttrValue(a,"ic-confirm");if((!d||confirm(d))&&("true"!=closestAttrValue(a,"ic-disable-when-doc-hidden")||!document.hidden)&&("true"!=closestAttrValue(a,"ic-disable-when-doc-inactive")||document.hasFocus())&&a.length>0){var e=uuid();a.data("ic-event-id",e);var f=function(){if(1==a.data("ic-request-in-flight"))return void a.data("ic-next-request",{req:f});if(a.data("ic-event-id")==e){var d=getStyleTarget(a),g=d?null:getAttrTarget(a),h=verbFor(a),i=getICAttribute(a,"ic-src");if(i){var j=b||function(b){d?a.css(d,b):g?a.attr(g,b):(processICResponse(b,a,!1,i),"GET"!=h&&refreshDependencies(getICAttribute(a,"ic-src"),a))},k=getParametersForElement(h,a,c);k&&handleRemoteRequest(a,h,i,k,j)}maybeInvokeLocalAction(a,"")}},g=closestAttrValue(a,"ic-trigger-delay");g?setTimeout(f,parseInterval(g)):f()}}function maybeInvokeLocalAction(a,b){var c=getICAttribute(a,"ic"+b+"-action");c&&invokeLocalAction(a,c,b)}function invokeLocalAction(a,b,c){var d=closestAttrValue(a,"ic"+c+"-action-target");null===d&&""!==c&&(d=closestAttrValue(a,"ic-action-target"));var e=null;e=d?getTargetImpl(a,"ic-action-target"):getTarget(a);var f=b.split(";"),g=[],h=0;$.each(f,function(a,b){var c=$.trim(b),d=c,f=[];c.indexOf(":")>0&&(d=c.substr(0,c.indexOf(":")),f=computeArgs(c.substr(c.indexOf(":")+1,c.length))),""==d||("delay"==d?(null==h&&(h=0),h+=parseInterval(f[0]+"")):(null==h&&(h=420),g.push([h,makeApplyAction(e,d,f)]),h=null))}),h=0,$.each(g,function(a,b){h+=b[0],setTimeout(b[1],h)})}function computeArgs(args){try{return eval("["+args+"]")}catch(a){return[$.trim(args)]}}function makeApplyAction(a,b,c){return function(){var d=a[b]||window[b];d?d.apply(a,c):log(a,"Action "+b+" was not found","ERROR")}}function newIntercoolerHistory(a,b,c,d){function e(a){return null==a||a.slotLimit!=c||a.historyVersion!=d||null==a.lruList}function f(){for(var b=[],e=0;e=0)log(e,"URL found in LRU list, moving to end","INFO"),c.splice(d,1),c.push(b);else if(log(e,"URL not found in LRU list, adding","INFO"),c.push(b),c.length>t.slotLimit){var f=c.shift();log(e,"History overflow, removing local history for "+f,"INFO"),a.removeItem(s+f)}return a.setItem(r,JSON.stringify(t)),c}function h(b){var d=JSON.stringify(b);try{a.setItem(b.id,d)}catch(e){try{f(),a.setItem(b.id,d)}catch(a){log(n($("body")),"Unable to save intercooler history with entire history cleared, is something else eating local storage? History Limit:"+c,"ERROR")}}}function i(a,b,c){var d={url:c,id:s+c,content:a,yOffset:b,timestamp:(new Date).getTime()};return g(c),h(d),d}function j(a){if(null==a.onpopstate||1!=a.onpopstate["ic-on-pop-state-handler"]){var b=a.onpopstate;a.onpopstate=function(a){triggerEvent(n($("body")),"handle.onpopstate.ic"),m(a)||b&&b(a),triggerEvent(n($("body")),"pageLoad.ic")},a.onpopstate["ic-on-pop-state-handler"]=!0}}function k(){u&&(l(u.newUrl,currentUrl(),u.oldHtml,u.yOffset),u=null)}function l(a,c,d,e){var f=i(d,e,c);b.replaceState({"ic-id":f.id},"","");var g=n($("body")),h=i(g.html(),window.pageYOffset,a);b.pushState({"ic-id":h.id},"",a),triggerEvent(g,"pushUrl.ic",[g,h])}function m(b){var c=b.state;if(c&&c["ic-id"]){var d=JSON.parse(a.getItem(c["ic-id"]));if(d)return processICResponse(d.content,n($("body")),!0),d.yOffset&&window.scrollTo(0,d.yOffset),!0;$.get(currentUrl(),{"ic-restore-history":!0},function(a,b){var c=createDocument(a),d=n(c).html();processICResponse(d,n($("body")),!0)})}return!1}function n(a){var b=a.find(getICAttributeSelector("ic-history-elt"));return b.length>0?b:a}function o(a){var b=n($("body"));triggerEvent(b,"beforeHistorySnapshot.ic",[b]),u={newUrl:a,oldHtml:b.html(),yOffset:window.pageYOffset}}function p(){var b="",c=[];for(var d in a)c.push(d);c.sort();var e=0;for(var f in c){var g=2*a[c[f]].length;e+=g,b+=c[f]+"="+(g/1024/1024).toFixed(2)+" MB\n"}return b+"\nTOTAL LOCAL STORAGE: "+(e/1024/1024).toFixed(2)+" MB"}function q(){return t}var r="ic-history-support",s="ic-hist-elt-",t=JSON.parse(a.getItem(r)),u=null;return e(t)&&(log(n($("body")),"Intercooler History configuration changed, clearing history","INFO"),f()),null==t&&(t={slotLimit:c,historyVersion:d,lruList:[]}),{clearHistory:f,updateHistory:k,addPopStateHandler:j,snapshotForHistory:o,_internal:{addPopStateHandler:j,supportData:q,dumpLocalStorage:p,updateLRUList:g}}}function getSlotLimit(){return 20}function refresh(a){return"string"==typeof a||a instanceof String?refreshDependencies(a):fireICRequest(a),Intercooler}function init(){var a=$("body");processNodes(a),fireReadyStuff(a),_history&&_history.addPopStateHandler(window),$.zepto&&($("body").data("zeptoDataTest",{}),"string"==typeof $("body").data("zeptoDataTest")&&console.log("!!!! Please include the data module with Zepto! Intercooler requires full data support to function !!!!")),location.search&&location.search.indexOf("ic-launch-debugger=true")>=0&&Intercooler.debug()}"undefined"!=typeof Zepto&&null==$&&($=Zepto);var USE_DATA="true"==$('meta[name="intercoolerjs:use-data-prefix"]').attr("content"),USE_ACTUAL_HTTP_METHOD="true"==$('meta[name="intercoolerjs:use-actual-http-method"]').attr("content"),_MACROS=$.map(["ic-get-from","ic-post-to","ic-put-to","ic-patch-to","ic-delete-from","ic-style-src","ic-attr-src","ic-prepend-from","ic-append-from","ic-action"],function(a){return fixICAttributeName(a)}),_scrollHandler=null,_UUID=1,_readyHandlers=[],_isDependentFunction=function(a,b){if(!a||!b)return!1;var c=a.split(/[\?#]/,1)[0].split("/").filter(function(a){return""!=a}),d=b.split(/[\?#]/,1)[0].split("/").filter(function(a){return""!=a});return""!=c&&""!=d&&(d.slice(0,c.length).join("/")==c.join("/")||c.slice(0,d.length).join("/")==d.join("/"))},_history=null;try{_history=newIntercoolerHistory(localStorage,window.history,getSlotLimit(),.1)}catch(a){log($("body"),"Could not initialize history","WARN")}return $.ajaxTransport&&$.ajaxTransport("text",function(a,b){if("#"==b.url[0]){var c=fixICAttributeName("ic-local-"),d=$(b.url),e=[],f=200,g="OK";d.each(function(a,b){$.each(b.attributes,function(a,b){if(b.name.substr(0,c.length)==c){var d=b.name.substring(c.length);if("status"==d){var h=b.value.match(/(\d+)\s?(.*)/);null!=h?(f=h[1],g=h[2]):(f="500",g="Attribute Error")}else e.push(d+": "+b.value)}})});var h=d.length>0?d.html():"";return{send:function(a,b){b(f,g,{html:h},e.join("\n"))},abort:function(){}}}return null}),$(function(){init()}),{refresh:refresh,history:_history,triggerRequest:fireICRequest,processNodes:processNodes,closestAttrValue:closestAttrValue,verbFor:verbFor,isDependent:isDependent,getTarget:getTarget,processHeaders:processHeaders,setIsDependentFunction:function(a){_isDependentFunction=a},ready:function(a){_readyHandlers.push(a)},debug:function(){var a=closestAttrValue("body","ic-debugger-url")||"https://intercoolerreleases-leaddynocom.netdna-ssl.com/intercooler-debugger.js";$.getScript(a).fail(function(a,b,c){log($("body"),formatError(c),"ERROR")})},_internal:{init:init,replaceOrAddMethod:replaceOrAddMethod,initEventSource:function(a){return new EventSource(a)},globalEval:globalEval}}}();return Intercooler}); \ No newline at end of file diff --git a/app/static/plugins/jquery/jquery-ui.css b/app/static/plugins/jquery/jquery-ui.css new file mode 100644 index 00000000..ffb94cdc --- /dev/null +++ b/app/static/plugins/jquery/jquery-ui.css @@ -0,0 +1,1311 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; + pointer-events: none; +} + + +/* Icons +----------------------------------*/ +.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin: 2px 0 0 0; + padding: .5em .5em .5em .7em; + font-size: 100%; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: 0; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + cursor: pointer; + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-item-wrapper { + position: relative; + padding: 3px 1em 3px .4em; +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item-wrapper { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-button { + padding: .4em 1em; + display: inline-block; + position: relative; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + /* Support: IE <= 11 */ + overflow: visible; +} + +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} + +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2em; + box-sizing: border-box; + text-indent: -9999px; + white-space: nowrap; +} + +/* no icon support for input elements */ +input.ui-button.ui-button-icon-only { + text-indent: 0; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon { + position: absolute; + top: 50%; + left: 50%; + margin-top: -8px; + margin-left: -8px; +} + +.ui-button.ui-icon-notext .ui-icon { + padding: 0; + width: 2.1em; + height: 2.1em; + text-indent: -9999px; + white-space: nowrap; + +} + +input.ui-button.ui-icon-notext .ui-icon { + width: auto; + height: auto; + text-indent: 0; + white-space: normal; + padding: .4em 1em; +} + +/* workarounds */ +/* Support: Firefox 5 - 40 */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-controlgroup { + vertical-align: middle; + display: inline-block; +} +.ui-controlgroup > .ui-controlgroup-item { + float: left; + margin-left: 0; + margin-right: 0; +} +.ui-controlgroup > .ui-controlgroup-item:focus, +.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus { + z-index: 9999; +} +.ui-controlgroup-vertical > .ui-controlgroup-item { + display: block; + float: none; + width: 100%; + margin-top: 0; + margin-bottom: 0; + text-align: left; +} +.ui-controlgroup-vertical .ui-controlgroup-item { + box-sizing: border-box; +} +.ui-controlgroup .ui-controlgroup-label { + padding: .4em 1em; +} +.ui-controlgroup .ui-controlgroup-label span { + font-size: 80%; +} +.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item { + border-left: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item { + border-top: none; +} +.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content { + border-right: none; +} +.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content { + border-bottom: none; +} + +/* Spinner specific style fixes */ +.ui-controlgroup-vertical .ui-spinner-input { + + /* Support: IE8 only, Android < 4.4 only */ + width: 75%; + width: calc( 100% - 2.4em ); +} +.ui-controlgroup-vertical .ui-spinner .ui-spinner-up { + border-top-style: solid; +} + +.ui-checkboxradio-label .ui-icon-background { + box-shadow: inset 1px 1px 1px #ccc; + border-radius: .12em; + border: none; +} +.ui-checkboxradio-radio-label .ui-icon-background { + width: 16px; + height: 16px; + border-radius: 1em; + overflow: visible; + border: none; +} +.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon, +.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon { + background-image: none; + width: 8px; + height: 8px; + border-width: 4px; + border-style: solid; +} +.ui-checkboxradio-disabled { + pointer-events: none; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Icons */ +.ui-datepicker .ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; +} +.ui-dialog { + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 20px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-n { + height: 2px; + top: 0; +} +.ui-dialog .ui-resizable-e { + width: 2px; + right: 0; +} +.ui-dialog .ui-resizable-s { + height: 2px; + bottom: 0; +} +.ui-dialog .ui-resizable-w { + width: 2px; + left: 0; +} +.ui-dialog .ui-resizable-se, +.ui-dialog .ui-resizable-sw, +.ui-dialog .ui-resizable-ne, +.ui-dialog .ui-resizable-nw { + width: 7px; + height: 7px; +} +.ui-dialog .ui-resizable-se { + right: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-sw { + left: 0; + bottom: 0; +} +.ui-dialog .ui-resizable-ne { + right: 0; + top: 0; +} +.ui-dialog .ui-resizable-nw { + left: 0; + top: 0; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("data:image/gif;base64,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"); + height: 100%; + filter: alpha(opacity=25); /* support: IE8 */ + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-selectable { + -ms-touch-action: none; + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + overflow-x: hidden; + padding-bottom: 1px; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-text { + display: block; + margin-right: 20px; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-selectmenu-button.ui-button { + text-align: left; + white-space: nowrap; + width: 14em; +} +.ui-selectmenu-icon.ui-icon { + float: right; + margin-top: 0; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; + -ms-touch-action: none; + touch-action: none; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* support: IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-sortable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: .222em 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 2em; +} +.ui-spinner-button { + width: 1.6em; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to override default borders */ +.ui-spinner a.ui-spinner-button { + border-top-style: none; + border-bottom-style: none; + border-right-style: none; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav .ui-tabs-anchor { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { + cursor: text; +} +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; +} +body .ui-tooltip { + border-width: 2px; +} +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #c5c5c5; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #ffffff; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #dddddd; + background: #e9e9e9; + color: #333333; + font-weight: bold; +} +.ui-widget-header a { + color: #333333; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #c5c5c5; + background: #f6f6f6; + font-weight: normal; + color: #454545; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #454545; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #cccccc; + background: #ededed; + font-weight: normal; + color: #2b2b2b; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #2b2b2b; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #003eff; + background: #007fff; + font-weight: normal; + color: #ffffff; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #003eff; + background-color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #dad55e; + background: #fffa90; + color: #777620; +} +.ui-state-checked { + border: 1px solid #dad55e; + background: #fffa90; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #777620; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #f1a899; + background: #fddfdf; + color: #5f3f3f; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #5f3f3f; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #5f3f3f; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_444444_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_444444_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon { + background-image: url("images/ui-icons_555555_256x240.png"); +} +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { + background-image: url("images/ui-icons_777620_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_cc0000_256x240.png"); +} +.ui-button .ui-icon { + background-image: url("images/ui-icons_777777_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 3px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .3; + filter: Alpha(Opacity=30); /* support: IE8 */ +} +.ui-widget-shadow { + -webkit-box-shadow: 0px 0px 5px #666666; + box-shadow: 0px 0px 5px #666666; +} \ No newline at end of file diff --git a/app/static/plugins/jquery/jquery-ui.js b/app/static/plugins/jquery/jquery-ui.js new file mode 100644 index 00000000..02135523 --- /dev/null +++ b/app/static/plugins/jquery/jquery-ui.js @@ -0,0 +1,18706 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +$.ui = $.ui || {}; + +var version = $.ui.version = "1.12.1"; + + +/*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Widget +//>>group: Core +//>>description: Provides a factory for creating stateful widgets with a common API. +//>>docs: http://api.jqueryui.com/jQuery.widget/ +//>>demos: http://jqueryui.com/widget/ + + + +var widgetUuid = 0; +var widgetSlice = Array.prototype.slice; + +$.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; +} )( $.cleanData ); + +$.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { + + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; +}; + +$.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; + + if ( isMethodCall ) { + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } + + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? + + // Element within the document + element.ownerDocument : + + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; +} ); + +var widget = $.widget; + + +/*! + * jQuery UI Position 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/position/ + */ + +//>>label: Position +//>>group: Core +//>>description: Positions elements relative to other elements. +//>>docs: http://api.jqueryui.com/position/ +//>>demos: http://jqueryui.com/position/ + + +( function() { +var cachedScrollbarWidth, + max = Math.max, + abs = Math.abs, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+(\.[\d]+)?%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + +function getOffsets( offsets, width, height ) { + return [ + parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), + parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) + ]; +} + +function parseCss( element, property ) { + return parseInt( $.css( element, property ), 10 ) || 0; +} + +function getDimensions( elem ) { + var raw = elem[ 0 ]; + if ( raw.nodeType === 9 ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: 0, left: 0 } + }; + } + if ( $.isWindow( raw ) ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: elem.scrollTop(), left: elem.scrollLeft() } + }; + } + if ( raw.preventDefault ) { + return { + width: 0, + height: 0, + offset: { top: raw.pageY, left: raw.pageX } + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; +} + +$.position = { + scrollbarWidth: function() { + if ( cachedScrollbarWidth !== undefined ) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $( "
    " + + "
    " ), + innerDiv = div.children()[ 0 ]; + + $( "body" ).append( div ); + w1 = innerDiv.offsetWidth; + div.css( "overflow", "scroll" ); + + w2 = innerDiv.offsetWidth; + + if ( w1 === w2 ) { + w2 = div[ 0 ].clientWidth; + } + + div.remove(); + + return ( cachedScrollbarWidth = w1 - w2 ); + }, + getScrollInfo: function( within ) { + var overflowX = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-x" ), + overflowY = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-y" ), + hasOverflowX = overflowX === "scroll" || + ( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ), + hasOverflowY = overflowY === "scroll" || + ( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight ); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function( element ) { + var withinElement = $( element || window ), + isWindow = $.isWindow( withinElement[ 0 ] ), + isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, + hasOffset = !isWindow && !isDocument; + return { + element: withinElement, + isWindow: isWindow, + isDocument: isDocument, + offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: withinElement.outerWidth(), + height: withinElement.outerHeight() + }; + } +}; + +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // Make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, + target = $( options.of ), + within = $.position.getWithinInfo( options.within ), + scrollInfo = $.position.getScrollInfo( within ), + collision = ( options.collision || "flip" ).split( " " ), + offsets = {}; + + dimensions = getDimensions( target ); + if ( target[ 0 ].preventDefault ) { + + // Force left top to allow flipping + options.at = "left top"; + } + targetWidth = dimensions.width; + targetHeight = dimensions.height; + targetOffset = dimensions.offset; + + // Clone to reuse original targetOffset later + basePosition = $.extend( {}, targetOffset ); + + // Force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[ this ] || "" ).split( " " ), + horizontalOffset, + verticalOffset; + + if ( pos.length === 1 ) { + pos = rhorizontal.test( pos[ 0 ] ) ? + pos.concat( [ "center" ] ) : + rvertical.test( pos[ 0 ] ) ? + [ "center" ].concat( pos ) : + [ "center", "center" ]; + } + pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; + pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; + + // Calculate offsets + horizontalOffset = roffset.exec( pos[ 0 ] ); + verticalOffset = roffset.exec( pos[ 1 ] ); + offsets[ this ] = [ + horizontalOffset ? horizontalOffset[ 0 ] : 0, + verticalOffset ? verticalOffset[ 0 ] : 0 + ]; + + // Reduce to just the positions without the offsets + options[ this ] = [ + rposition.exec( pos[ 0 ] )[ 0 ], + rposition.exec( pos[ 1 ] )[ 0 ] + ]; + } ); + + // Normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + if ( options.at[ 0 ] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[ 0 ] === "center" ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[ 1 ] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[ 1 ] === "center" ) { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); + basePosition.left += atOffset[ 0 ]; + basePosition.top += atOffset[ 1 ]; + + return this.each( function() { + var collisionPosition, using, + elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss( this, "marginLeft" ), + marginTop = parseCss( this, "marginTop" ), + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + + scrollInfo.height, + position = $.extend( {}, basePosition ), + myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); + + if ( options.my[ 0 ] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[ 0 ] === "center" ) { + position.left -= elemWidth / 2; + } + + if ( options.my[ 1 ] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[ 1 ] === "center" ) { + position.top -= elemHeight / 2; + } + + position.left += myOffset[ 0 ]; + position.top += myOffset[ 1 ]; + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[ i ] ] ) { + $.ui.position[ collision[ i ] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], + my: options.my, + at: options.at, + within: within, + elem: elem + } ); + } + } ); + + if ( options.using ) { + + // Adds feedback as second argument to using callback, if present + using = function( props ) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { + feedback.horizontal = "center"; + } + if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { + feedback.vertical = "middle"; + } + if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call( this, props, feedback ); + }; + } + + elem.offset( $.extend( position, { using: using } ) ); + } ); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // Element is wider than within + if ( data.collisionWidth > outerWidth ) { + + // Element is initially over the left side of within + if ( overLeft > 0 && overRight <= 0 ) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - + withinOffset; + position.left += overLeft - newOverRight; + + // Element is initially over right side of within + } else if ( overRight > 0 && overLeft <= 0 ) { + position.left = withinOffset; + + // Element is initially over both left and right sides of within + } else { + if ( overLeft > overRight ) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + + // Too far left -> align with left edge + } else if ( overLeft > 0 ) { + position.left += overLeft; + + // Too far right -> align with right edge + } else if ( overRight > 0 ) { + position.left -= overRight; + + // Adjust based on position and margin + } else { + position.left = max( position.left - collisionPosLeft, position.left ); + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // Element is taller than within + if ( data.collisionHeight > outerHeight ) { + + // Element is initially over the top of within + if ( overTop > 0 && overBottom <= 0 ) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - + withinOffset; + position.top += overTop - newOverBottom; + + // Element is initially over bottom of within + } else if ( overBottom > 0 && overTop <= 0 ) { + position.top = withinOffset; + + // Element is initially over both top and bottom of within + } else { + if ( overTop > overBottom ) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + + // Too far up -> align with top + } else if ( overTop > 0 ) { + position.top += overTop; + + // Too far down -> align with bottom edge + } else if ( overBottom > 0 ) { + position.top -= overBottom; + + // Adjust based on position and margin + } else { + position.top = max( position.top - collisionPosTop, position.top ); + } + } + }, + flip: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + data.at[ 0 ] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[ 0 ], + newOverRight, + newOverLeft; + + if ( overLeft < 0 ) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - + outerWidth - withinOffset; + if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { + position.left += myOffset + atOffset + offset; + } + } else if ( overRight > 0 ) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + + atOffset + offset - offsetLeft; + if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[ 1 ] === "top", + myOffset = top ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + data.at[ 1 ] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[ 1 ], + newOverTop, + newOverBottom; + if ( overTop < 0 ) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - + outerHeight - withinOffset; + if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) { + position.top += myOffset + atOffset + offset; + } + } else if ( overBottom > 0 ) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + + offset - offsetTop; + if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function() { + $.ui.position.flip.left.apply( this, arguments ); + $.ui.position.fit.left.apply( this, arguments ); + }, + top: function() { + $.ui.position.flip.top.apply( this, arguments ); + $.ui.position.fit.top.apply( this, arguments ); + } + } +}; + +} )(); + +var position = $.ui.position; + + +/*! + * jQuery UI :data 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :data Selector +//>>group: Core +//>>description: Selects elements which have data stored under the specified key. +//>>docs: http://api.jqueryui.com/data-selector/ + + +var data = $.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + } ) : + + // Support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + } +} ); + +/*! + * jQuery UI Disable Selection 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: disableSelection +//>>group: Core +//>>description: Disable selection of text content within the set of matched elements. +//>>docs: http://api.jqueryui.com/disableSelection/ + +// This file is deprecated + + +var disableSelection = $.fn.extend( { + disableSelection: ( function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? + "selectstart" : + "mousedown"; + + return function() { + return this.on( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + } ); + }; + } )(), + + enableSelection: function() { + return this.off( ".ui-disableSelection" ); + } +} ); + + +/*! + * jQuery UI Effects 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Effects Core +//>>group: Effects +// jscs:disable maximumLineLength +//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. +// jscs:enable maximumLineLength +//>>docs: http://api.jqueryui.com/category/effects-core/ +//>>demos: http://jqueryui.com/effect/ + + + +var dataSpace = "ui-effects-", + dataSpaceStyle = "ui-effects-style", + dataSpaceAnimated = "ui-effects-animated", + + // Create a local jQuery because jQuery Color relies on it and the + // global may not exist with AMD and a custom build (#10199) + jQuery = $; + +$.effects = { + effect: {} +}; + +/*! + * jQuery Color Animations v2.1.2 + * https://github.com/jquery/jquery-color + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * Date: Wed Jan 16 08:47:09 2013 -0600 + */ +( function( jQuery, undefined ) { + + var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " + + "borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", + + // Plusequals test for += 100 -= 100 + rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, + + // A set of RE's that can match strings and generate color tuples. + stringParsers = [ { + re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ], + execResult[ 3 ], + execResult[ 4 ] + ]; + } + }, { + re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + parse: function( execResult ) { + return [ + execResult[ 1 ] * 2.55, + execResult[ 2 ] * 2.55, + execResult[ 3 ] * 2.55, + execResult[ 4 ] + ]; + } + }, { + + // This regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ], 16 ) + ]; + } + }, { + + // This regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, + parse: function( execResult ) { + return [ + parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), + parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), + parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) + ]; + } + }, { + re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, + space: "hsla", + parse: function( execResult ) { + return [ + execResult[ 1 ], + execResult[ 2 ] / 100, + execResult[ 3 ] / 100, + execResult[ 4 ] + ]; + } + } ], + + // JQuery.Color( ) + color = jQuery.Color = function( color, green, blue, alpha ) { + return new jQuery.Color.fn.parse( color, green, blue, alpha ); + }, + spaces = { + rgba: { + props: { + red: { + idx: 0, + type: "byte" + }, + green: { + idx: 1, + type: "byte" + }, + blue: { + idx: 2, + type: "byte" + } + } + }, + + hsla: { + props: { + hue: { + idx: 0, + type: "degrees" + }, + saturation: { + idx: 1, + type: "percent" + }, + lightness: { + idx: 2, + type: "percent" + } + } + } + }, + propTypes = { + "byte": { + floor: true, + max: 255 + }, + "percent": { + max: 1 + }, + "degrees": { + mod: 360, + floor: true + } + }, + support = color.support = {}, + + // Element for support tests + supportElem = jQuery( "

    " )[ 0 ], + + // Colors = jQuery.Color.names + colors, + + // Local aliases of functions called often + each = jQuery.each; + +// Determine rgba support immediately +supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; +support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; + +// Define cache name and alpha properties +// for rgba and hsla spaces +each( spaces, function( spaceName, space ) { + space.cache = "_" + spaceName; + space.props.alpha = { + idx: 3, + type: "percent", + def: 1 + }; +} ); + +function clamp( value, prop, allowEmpty ) { + var type = propTypes[ prop.type ] || {}; + + if ( value == null ) { + return ( allowEmpty || !prop.def ) ? null : prop.def; + } + + // ~~ is an short way of doing floor for positive numbers + value = type.floor ? ~~value : parseFloat( value ); + + // IE will pass in empty strings as value for alpha, + // which will hit this case + if ( isNaN( value ) ) { + return prop.def; + } + + if ( type.mod ) { + + // We add mod before modding to make sure that negatives values + // get converted properly: -10 -> 350 + return ( value + type.mod ) % type.mod; + } + + // For now all property types without mod have min and max + return 0 > value ? 0 : type.max < value ? type.max : value; +} + +function stringParse( string ) { + var inst = color(), + rgba = inst._rgba = []; + + string = string.toLowerCase(); + + each( stringParsers, function( i, parser ) { + var parsed, + match = parser.re.exec( string ), + values = match && parser.parse( match ), + spaceName = parser.space || "rgba"; + + if ( values ) { + parsed = inst[ spaceName ]( values ); + + // If this was an rgba parse the assignment might happen twice + // oh well.... + inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; + rgba = inst._rgba = parsed._rgba; + + // Exit each( stringParsers ) here because we matched + return false; + } + } ); + + // Found a stringParser that handled it + if ( rgba.length ) { + + // If this came from a parsed string, force "transparent" when alpha is 0 + // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) + if ( rgba.join() === "0,0,0,0" ) { + jQuery.extend( rgba, colors.transparent ); + } + return inst; + } + + // Named colors + return colors[ string ]; +} + +color.fn = jQuery.extend( color.prototype, { + parse: function( red, green, blue, alpha ) { + if ( red === undefined ) { + this._rgba = [ null, null, null, null ]; + return this; + } + if ( red.jquery || red.nodeType ) { + red = jQuery( red ).css( green ); + green = undefined; + } + + var inst = this, + type = jQuery.type( red ), + rgba = this._rgba = []; + + // More than 1 argument specified - assume ( red, green, blue, alpha ) + if ( green !== undefined ) { + red = [ red, green, blue, alpha ]; + type = "array"; + } + + if ( type === "string" ) { + return this.parse( stringParse( red ) || colors._default ); + } + + if ( type === "array" ) { + each( spaces.rgba.props, function( key, prop ) { + rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); + } ); + return this; + } + + if ( type === "object" ) { + if ( red instanceof color ) { + each( spaces, function( spaceName, space ) { + if ( red[ space.cache ] ) { + inst[ space.cache ] = red[ space.cache ].slice(); + } + } ); + } else { + each( spaces, function( spaceName, space ) { + var cache = space.cache; + each( space.props, function( key, prop ) { + + // If the cache doesn't exist, and we know how to convert + if ( !inst[ cache ] && space.to ) { + + // If the value was null, we don't need to copy it + // if the key was alpha, we don't need to copy it either + if ( key === "alpha" || red[ key ] == null ) { + return; + } + inst[ cache ] = space.to( inst._rgba ); + } + + // This is the only case where we allow nulls for ALL properties. + // call clamp with alwaysAllowEmpty + inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); + } ); + + // Everything defined but alpha? + if ( inst[ cache ] && + jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + + // Use the default of 1 + inst[ cache ][ 3 ] = 1; + if ( space.from ) { + inst._rgba = space.from( inst[ cache ] ); + } + } + } ); + } + return this; + } + }, + is: function( compare ) { + var is = color( compare ), + same = true, + inst = this; + + each( spaces, function( _, space ) { + var localCache, + isCache = is[ space.cache ]; + if ( isCache ) { + localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || []; + each( space.props, function( _, prop ) { + if ( isCache[ prop.idx ] != null ) { + same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); + return same; + } + } ); + } + return same; + } ); + return same; + }, + _space: function() { + var used = [], + inst = this; + each( spaces, function( spaceName, space ) { + if ( inst[ space.cache ] ) { + used.push( spaceName ); + } + } ); + return used.pop(); + }, + transition: function( other, distance ) { + var end = color( other ), + spaceName = end._space(), + space = spaces[ spaceName ], + startColor = this.alpha() === 0 ? color( "transparent" ) : this, + start = startColor[ space.cache ] || space.to( startColor._rgba ), + result = start.slice(); + + end = end[ space.cache ]; + each( space.props, function( key, prop ) { + var index = prop.idx, + startValue = start[ index ], + endValue = end[ index ], + type = propTypes[ prop.type ] || {}; + + // If null, don't override start value + if ( endValue === null ) { + return; + } + + // If null - use end + if ( startValue === null ) { + result[ index ] = endValue; + } else { + if ( type.mod ) { + if ( endValue - startValue > type.mod / 2 ) { + startValue += type.mod; + } else if ( startValue - endValue > type.mod / 2 ) { + startValue -= type.mod; + } + } + result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); + } + } ); + return this[ spaceName ]( result ); + }, + blend: function( opaque ) { + + // If we are already opaque - return ourself + if ( this._rgba[ 3 ] === 1 ) { + return this; + } + + var rgb = this._rgba.slice(), + a = rgb.pop(), + blend = color( opaque )._rgba; + + return color( jQuery.map( rgb, function( v, i ) { + return ( 1 - a ) * blend[ i ] + a * v; + } ) ); + }, + toRgbaString: function() { + var prefix = "rgba(", + rgba = jQuery.map( this._rgba, function( v, i ) { + return v == null ? ( i > 2 ? 1 : 0 ) : v; + } ); + + if ( rgba[ 3 ] === 1 ) { + rgba.pop(); + prefix = "rgb("; + } + + return prefix + rgba.join() + ")"; + }, + toHslaString: function() { + var prefix = "hsla(", + hsla = jQuery.map( this.hsla(), function( v, i ) { + if ( v == null ) { + v = i > 2 ? 1 : 0; + } + + // Catch 1 and 2 + if ( i && i < 3 ) { + v = Math.round( v * 100 ) + "%"; + } + return v; + } ); + + if ( hsla[ 3 ] === 1 ) { + hsla.pop(); + prefix = "hsl("; + } + return prefix + hsla.join() + ")"; + }, + toHexString: function( includeAlpha ) { + var rgba = this._rgba.slice(), + alpha = rgba.pop(); + + if ( includeAlpha ) { + rgba.push( ~~( alpha * 255 ) ); + } + + return "#" + jQuery.map( rgba, function( v ) { + + // Default to 0 when nulls exist + v = ( v || 0 ).toString( 16 ); + return v.length === 1 ? "0" + v : v; + } ).join( "" ); + }, + toString: function() { + return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); + } +} ); +color.fn.parse.prototype = color.fn; + +// Hsla conversions adapted from: +// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 + +function hue2rgb( p, q, h ) { + h = ( h + 1 ) % 1; + if ( h * 6 < 1 ) { + return p + ( q - p ) * h * 6; + } + if ( h * 2 < 1 ) { + return q; + } + if ( h * 3 < 2 ) { + return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6; + } + return p; +} + +spaces.hsla.to = function( rgba ) { + if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { + return [ null, null, null, rgba[ 3 ] ]; + } + var r = rgba[ 0 ] / 255, + g = rgba[ 1 ] / 255, + b = rgba[ 2 ] / 255, + a = rgba[ 3 ], + max = Math.max( r, g, b ), + min = Math.min( r, g, b ), + diff = max - min, + add = max + min, + l = add * 0.5, + h, s; + + if ( min === max ) { + h = 0; + } else if ( r === max ) { + h = ( 60 * ( g - b ) / diff ) + 360; + } else if ( g === max ) { + h = ( 60 * ( b - r ) / diff ) + 120; + } else { + h = ( 60 * ( r - g ) / diff ) + 240; + } + + // Chroma (diff) == 0 means greyscale which, by definition, saturation = 0% + // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) + if ( diff === 0 ) { + s = 0; + } else if ( l <= 0.5 ) { + s = diff / add; + } else { + s = diff / ( 2 - add ); + } + return [ Math.round( h ) % 360, s, l, a == null ? 1 : a ]; +}; + +spaces.hsla.from = function( hsla ) { + if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { + return [ null, null, null, hsla[ 3 ] ]; + } + var h = hsla[ 0 ] / 360, + s = hsla[ 1 ], + l = hsla[ 2 ], + a = hsla[ 3 ], + q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, + p = 2 * l - q; + + return [ + Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), + Math.round( hue2rgb( p, q, h ) * 255 ), + Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), + a + ]; +}; + +each( spaces, function( spaceName, space ) { + var props = space.props, + cache = space.cache, + to = space.to, + from = space.from; + + // Makes rgba() and hsla() + color.fn[ spaceName ] = function( value ) { + + // Generate a cache for this space if it doesn't exist + if ( to && !this[ cache ] ) { + this[ cache ] = to( this._rgba ); + } + if ( value === undefined ) { + return this[ cache ].slice(); + } + + var ret, + type = jQuery.type( value ), + arr = ( type === "array" || type === "object" ) ? value : arguments, + local = this[ cache ].slice(); + + each( props, function( key, prop ) { + var val = arr[ type === "object" ? key : prop.idx ]; + if ( val == null ) { + val = local[ prop.idx ]; + } + local[ prop.idx ] = clamp( val, prop ); + } ); + + if ( from ) { + ret = color( from( local ) ); + ret[ cache ] = local; + return ret; + } else { + return color( local ); + } + }; + + // Makes red() green() blue() alpha() hue() saturation() lightness() + each( props, function( key, prop ) { + + // Alpha is included in more than one space + if ( color.fn[ key ] ) { + return; + } + color.fn[ key ] = function( value ) { + var vtype = jQuery.type( value ), + fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ), + local = this[ fn ](), + cur = local[ prop.idx ], + match; + + if ( vtype === "undefined" ) { + return cur; + } + + if ( vtype === "function" ) { + value = value.call( this, cur ); + vtype = jQuery.type( value ); + } + if ( value == null && prop.empty ) { + return this; + } + if ( vtype === "string" ) { + match = rplusequals.exec( value ); + if ( match ) { + value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); + } + } + local[ prop.idx ] = value; + return this[ fn ]( local ); + }; + } ); +} ); + +// Add cssHook and .fx.step function for each named hook. +// accept a space separated string of properties +color.hook = function( hook ) { + var hooks = hook.split( " " ); + each( hooks, function( i, hook ) { + jQuery.cssHooks[ hook ] = { + set: function( elem, value ) { + var parsed, curElem, + backgroundColor = ""; + + if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || + ( parsed = stringParse( value ) ) ) ) { + value = color( parsed || value ); + if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { + curElem = hook === "backgroundColor" ? elem.parentNode : elem; + while ( + ( backgroundColor === "" || backgroundColor === "transparent" ) && + curElem && curElem.style + ) { + try { + backgroundColor = jQuery.css( curElem, "backgroundColor" ); + curElem = curElem.parentNode; + } catch ( e ) { + } + } + + value = value.blend( backgroundColor && backgroundColor !== "transparent" ? + backgroundColor : + "_default" ); + } + + value = value.toRgbaString(); + } + try { + elem.style[ hook ] = value; + } catch ( e ) { + + // Wrapped to prevent IE from throwing errors on "invalid" values like + // 'auto' or 'inherit' + } + } + }; + jQuery.fx.step[ hook ] = function( fx ) { + if ( !fx.colorInit ) { + fx.start = color( fx.elem, hook ); + fx.end = color( fx.end ); + fx.colorInit = true; + } + jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); + }; + } ); + +}; + +color.hook( stepHooks ); + +jQuery.cssHooks.borderColor = { + expand: function( value ) { + var expanded = {}; + + each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) { + expanded[ "border" + part + "Color" ] = value; + } ); + return expanded; + } +}; + +// Basic color names only. +// Usage of any of the other color names requires adding yourself or including +// jquery.color.svg-names.js. +colors = jQuery.Color.names = { + + // 4.1. Basic color keywords + aqua: "#00ffff", + black: "#000000", + blue: "#0000ff", + fuchsia: "#ff00ff", + gray: "#808080", + green: "#008000", + lime: "#00ff00", + maroon: "#800000", + navy: "#000080", + olive: "#808000", + purple: "#800080", + red: "#ff0000", + silver: "#c0c0c0", + teal: "#008080", + white: "#ffffff", + yellow: "#ffff00", + + // 4.2.3. "transparent" color keyword + transparent: [ null, null, null, 0 ], + + _default: "#ffffff" +}; + +} )( jQuery ); + +/******************************************************************************/ +/****************************** CLASS ANIMATIONS ******************************/ +/******************************************************************************/ +( function() { + +var classAnimationActions = [ "add", "remove", "toggle" ], + shorthandStyles = { + border: 1, + borderBottom: 1, + borderColor: 1, + borderLeft: 1, + borderRight: 1, + borderTop: 1, + borderWidth: 1, + margin: 1, + padding: 1 + }; + +$.each( + [ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], + function( _, prop ) { + $.fx.step[ prop ] = function( fx ) { + if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) { + jQuery.style( fx.elem, prop, fx.end ); + fx.setAttr = true; + } + }; + } +); + +function getElementStyles( elem ) { + var key, len, + style = elem.ownerDocument.defaultView ? + elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : + elem.currentStyle, + styles = {}; + + if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { + len = style.length; + while ( len-- ) { + key = style[ len ]; + if ( typeof style[ key ] === "string" ) { + styles[ $.camelCase( key ) ] = style[ key ]; + } + } + + // Support: Opera, IE <9 + } else { + for ( key in style ) { + if ( typeof style[ key ] === "string" ) { + styles[ key ] = style[ key ]; + } + } + } + + return styles; +} + +function styleDifference( oldStyle, newStyle ) { + var diff = {}, + name, value; + + for ( name in newStyle ) { + value = newStyle[ name ]; + if ( oldStyle[ name ] !== value ) { + if ( !shorthandStyles[ name ] ) { + if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) { + diff[ name ] = value; + } + } + } + } + + return diff; +} + +// Support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +$.effects.animateClass = function( value, duration, easing, callback ) { + var o = $.speed( duration, easing, callback ); + + return this.queue( function() { + var animated = $( this ), + baseClass = animated.attr( "class" ) || "", + applyClassChange, + allAnimations = o.children ? animated.find( "*" ).addBack() : animated; + + // Map the animated objects to store the original styles. + allAnimations = allAnimations.map( function() { + var el = $( this ); + return { + el: el, + start: getElementStyles( this ) + }; + } ); + + // Apply class change + applyClassChange = function() { + $.each( classAnimationActions, function( i, action ) { + if ( value[ action ] ) { + animated[ action + "Class" ]( value[ action ] ); + } + } ); + }; + applyClassChange(); + + // Map all animated objects again - calculate new styles and diff + allAnimations = allAnimations.map( function() { + this.end = getElementStyles( this.el[ 0 ] ); + this.diff = styleDifference( this.start, this.end ); + return this; + } ); + + // Apply original class + animated.attr( "class", baseClass ); + + // Map all animated objects again - this time collecting a promise + allAnimations = allAnimations.map( function() { + var styleInfo = this, + dfd = $.Deferred(), + opts = $.extend( {}, o, { + queue: false, + complete: function() { + dfd.resolve( styleInfo ); + } + } ); + + this.el.animate( this.diff, opts ); + return dfd.promise(); + } ); + + // Once all animations have completed: + $.when.apply( $, allAnimations.get() ).done( function() { + + // Set the final class + applyClassChange(); + + // For each animated element, + // clear all css properties that were animated + $.each( arguments, function() { + var el = this.el; + $.each( this.diff, function( key ) { + el.css( key, "" ); + } ); + } ); + + // This is guarnteed to be there if you use jQuery.speed() + // it also handles dequeuing the next anim... + o.complete.call( animated[ 0 ] ); + } ); + } ); +}; + +$.fn.extend( { + addClass: ( function( orig ) { + return function( classNames, speed, easing, callback ) { + return speed ? + $.effects.animateClass.call( this, + { add: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + } )( $.fn.addClass ), + + removeClass: ( function( orig ) { + return function( classNames, speed, easing, callback ) { + return arguments.length > 1 ? + $.effects.animateClass.call( this, + { remove: classNames }, speed, easing, callback ) : + orig.apply( this, arguments ); + }; + } )( $.fn.removeClass ), + + toggleClass: ( function( orig ) { + return function( classNames, force, speed, easing, callback ) { + if ( typeof force === "boolean" || force === undefined ) { + if ( !speed ) { + + // Without speed parameter + return orig.apply( this, arguments ); + } else { + return $.effects.animateClass.call( this, + ( force ? { add: classNames } : { remove: classNames } ), + speed, easing, callback ); + } + } else { + + // Without force parameter + return $.effects.animateClass.call( this, + { toggle: classNames }, force, speed, easing ); + } + }; + } )( $.fn.toggleClass ), + + switchClass: function( remove, add, speed, easing, callback ) { + return $.effects.animateClass.call( this, { + add: add, + remove: remove + }, speed, easing, callback ); + } +} ); + +} )(); + +/******************************************************************************/ +/*********************************** EFFECTS **********************************/ +/******************************************************************************/ + +( function() { + +if ( $.expr && $.expr.filters && $.expr.filters.animated ) { + $.expr.filters.animated = ( function( orig ) { + return function( elem ) { + return !!$( elem ).data( dataSpaceAnimated ) || orig( elem ); + }; + } )( $.expr.filters.animated ); +} + +if ( $.uiBackCompat !== false ) { + $.extend( $.effects, { + + // Saves a set of properties in a data storage + save: function( element, set ) { + var i = 0, length = set.length; + for ( ; i < length; i++ ) { + if ( set[ i ] !== null ) { + element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] ); + } + } + }, + + // Restores a set of previously saved properties from a data storage + restore: function( element, set ) { + var val, i = 0, length = set.length; + for ( ; i < length; i++ ) { + if ( set[ i ] !== null ) { + val = element.data( dataSpace + set[ i ] ); + element.css( set[ i ], val ); + } + } + }, + + setMode: function( el, mode ) { + if ( mode === "toggle" ) { + mode = el.is( ":hidden" ) ? "show" : "hide"; + } + return mode; + }, + + // Wraps the element around a wrapper that copies position properties + createWrapper: function( element ) { + + // If the element is already wrapped, return it + if ( element.parent().is( ".ui-effects-wrapper" ) ) { + return element.parent(); + } + + // Wrap the element + var props = { + width: element.outerWidth( true ), + height: element.outerHeight( true ), + "float": element.css( "float" ) + }, + wrapper = $( "

    " ) + .addClass( "ui-effects-wrapper" ) + .css( { + fontSize: "100%", + background: "transparent", + border: "none", + margin: 0, + padding: 0 + } ), + + // Store the size in case width/height are defined in % - Fixes #5245 + size = { + width: element.width(), + height: element.height() + }, + active = document.activeElement; + + // Support: Firefox + // Firefox incorrectly exposes anonymous content + // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 + try { + active.id; + } catch ( e ) { + active = document.body; + } + + element.wrap( wrapper ); + + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).trigger( "focus" ); + } + + // Hotfix for jQuery 1.4 since some change in wrap() seems to actually + // lose the reference to the wrapped element + wrapper = element.parent(); + + // Transfer positioning properties to the wrapper + if ( element.css( "position" ) === "static" ) { + wrapper.css( { position: "relative" } ); + element.css( { position: "relative" } ); + } else { + $.extend( props, { + position: element.css( "position" ), + zIndex: element.css( "z-index" ) + } ); + $.each( [ "top", "left", "bottom", "right" ], function( i, pos ) { + props[ pos ] = element.css( pos ); + if ( isNaN( parseInt( props[ pos ], 10 ) ) ) { + props[ pos ] = "auto"; + } + } ); + element.css( { + position: "relative", + top: 0, + left: 0, + right: "auto", + bottom: "auto" + } ); + } + element.css( size ); + + return wrapper.css( props ).show(); + }, + + removeWrapper: function( element ) { + var active = document.activeElement; + + if ( element.parent().is( ".ui-effects-wrapper" ) ) { + element.parent().replaceWith( element ); + + // Fixes #7595 - Elements lose focus when wrapped. + if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { + $( active ).trigger( "focus" ); + } + } + + return element; + } + } ); +} + +$.extend( $.effects, { + version: "1.12.1", + + define: function( name, mode, effect ) { + if ( !effect ) { + effect = mode; + mode = "effect"; + } + + $.effects.effect[ name ] = effect; + $.effects.effect[ name ].mode = mode; + + return effect; + }, + + scaledDimensions: function( element, percent, direction ) { + if ( percent === 0 ) { + return { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + }; + } + + var x = direction !== "horizontal" ? ( ( percent || 100 ) / 100 ) : 1, + y = direction !== "vertical" ? ( ( percent || 100 ) / 100 ) : 1; + + return { + height: element.height() * y, + width: element.width() * x, + outerHeight: element.outerHeight() * y, + outerWidth: element.outerWidth() * x + }; + + }, + + clipToBox: function( animation ) { + return { + width: animation.clip.right - animation.clip.left, + height: animation.clip.bottom - animation.clip.top, + left: animation.clip.left, + top: animation.clip.top + }; + }, + + // Injects recently queued functions to be first in line (after "inprogress") + unshift: function( element, queueLength, count ) { + var queue = element.queue(); + + if ( queueLength > 1 ) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queueLength, count ) ) ); + } + element.dequeue(); + }, + + saveStyle: function( element ) { + element.data( dataSpaceStyle, element[ 0 ].style.cssText ); + }, + + restoreStyle: function( element ) { + element[ 0 ].style.cssText = element.data( dataSpaceStyle ) || ""; + element.removeData( dataSpaceStyle ); + }, + + mode: function( element, mode ) { + var hidden = element.is( ":hidden" ); + + if ( mode === "toggle" ) { + mode = hidden ? "show" : "hide"; + } + if ( hidden ? mode === "hide" : mode === "show" ) { + mode = "none"; + } + return mode; + }, + + // Translates a [top,left] array into a baseline value + getBaseline: function( origin, original ) { + var y, x; + + switch ( origin[ 0 ] ) { + case "top": + y = 0; + break; + case "middle": + y = 0.5; + break; + case "bottom": + y = 1; + break; + default: + y = origin[ 0 ] / original.height; + } + + switch ( origin[ 1 ] ) { + case "left": + x = 0; + break; + case "center": + x = 0.5; + break; + case "right": + x = 1; + break; + default: + x = origin[ 1 ] / original.width; + } + + return { + x: x, + y: y + }; + }, + + // Creates a placeholder element so that the original element can be made absolute + createPlaceholder: function( element ) { + var placeholder, + cssPosition = element.css( "position" ), + position = element.position(); + + // Lock in margins first to account for form elements, which + // will change margin if you explicitly set height + // see: http://jsfiddle.net/JZSMt/3/ https://bugs.webkit.org/show_bug.cgi?id=107380 + // Support: Safari + element.css( { + marginTop: element.css( "marginTop" ), + marginBottom: element.css( "marginBottom" ), + marginLeft: element.css( "marginLeft" ), + marginRight: element.css( "marginRight" ) + } ) + .outerWidth( element.outerWidth() ) + .outerHeight( element.outerHeight() ); + + if ( /^(static|relative)/.test( cssPosition ) ) { + cssPosition = "absolute"; + + placeholder = $( "<" + element[ 0 ].nodeName + ">" ).insertAfter( element ).css( { + + // Convert inline to inline block to account for inline elements + // that turn to inline block based on content (like img) + display: /^(inline|ruby)/.test( element.css( "display" ) ) ? + "inline-block" : + "block", + visibility: "hidden", + + // Margins need to be set to account for margin collapse + marginTop: element.css( "marginTop" ), + marginBottom: element.css( "marginBottom" ), + marginLeft: element.css( "marginLeft" ), + marginRight: element.css( "marginRight" ), + "float": element.css( "float" ) + } ) + .outerWidth( element.outerWidth() ) + .outerHeight( element.outerHeight() ) + .addClass( "ui-effects-placeholder" ); + + element.data( dataSpace + "placeholder", placeholder ); + } + + element.css( { + position: cssPosition, + left: position.left, + top: position.top + } ); + + return placeholder; + }, + + removePlaceholder: function( element ) { + var dataKey = dataSpace + "placeholder", + placeholder = element.data( dataKey ); + + if ( placeholder ) { + placeholder.remove(); + element.removeData( dataKey ); + } + }, + + // Removes a placeholder if it exists and restores + // properties that were modified during placeholder creation + cleanUp: function( element ) { + $.effects.restoreStyle( element ); + $.effects.removePlaceholder( element ); + }, + + setTransition: function( element, list, factor, value ) { + value = value || {}; + $.each( list, function( i, x ) { + var unit = element.cssUnit( x ); + if ( unit[ 0 ] > 0 ) { + value[ x ] = unit[ 0 ] * factor + unit[ 1 ]; + } + } ); + return value; + } +} ); + +// Return an effect options object for the given parameters: +function _normalizeArguments( effect, options, speed, callback ) { + + // Allow passing all options as the first parameter + if ( $.isPlainObject( effect ) ) { + options = effect; + effect = effect.effect; + } + + // Convert to an object + effect = { effect: effect }; + + // Catch (effect, null, ...) + if ( options == null ) { + options = {}; + } + + // Catch (effect, callback) + if ( $.isFunction( options ) ) { + callback = options; + speed = null; + options = {}; + } + + // Catch (effect, speed, ?) + if ( typeof options === "number" || $.fx.speeds[ options ] ) { + callback = speed; + speed = options; + options = {}; + } + + // Catch (effect, options, callback) + if ( $.isFunction( speed ) ) { + callback = speed; + speed = null; + } + + // Add options to effect + if ( options ) { + $.extend( effect, options ); + } + + speed = speed || options.duration; + effect.duration = $.fx.off ? 0 : + typeof speed === "number" ? speed : + speed in $.fx.speeds ? $.fx.speeds[ speed ] : + $.fx.speeds._default; + + effect.complete = callback || options.complete; + + return effect; +} + +function standardAnimationOption( option ) { + + // Valid standard speeds (nothing, number, named speed) + if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) { + return true; + } + + // Invalid strings - treat as "normal" speed + if ( typeof option === "string" && !$.effects.effect[ option ] ) { + return true; + } + + // Complete callback + if ( $.isFunction( option ) ) { + return true; + } + + // Options hash (but not naming an effect) + if ( typeof option === "object" && !option.effect ) { + return true; + } + + // Didn't match any standard API + return false; +} + +$.fn.extend( { + effect: function( /* effect, options, speed, callback */ ) { + var args = _normalizeArguments.apply( this, arguments ), + effectMethod = $.effects.effect[ args.effect ], + defaultMode = effectMethod.mode, + queue = args.queue, + queueName = queue || "fx", + complete = args.complete, + mode = args.mode, + modes = [], + prefilter = function( next ) { + var el = $( this ), + normalizedMode = $.effects.mode( el, mode ) || defaultMode; + + // Sentinel for duck-punching the :animated psuedo-selector + el.data( dataSpaceAnimated, true ); + + // Save effect mode for later use, + // we can't just call $.effects.mode again later, + // as the .show() below destroys the initial state + modes.push( normalizedMode ); + + // See $.uiBackCompat inside of run() for removal of defaultMode in 1.13 + if ( defaultMode && ( normalizedMode === "show" || + ( normalizedMode === defaultMode && normalizedMode === "hide" ) ) ) { + el.show(); + } + + if ( !defaultMode || normalizedMode !== "none" ) { + $.effects.saveStyle( el ); + } + + if ( $.isFunction( next ) ) { + next(); + } + }; + + if ( $.fx.off || !effectMethod ) { + + // Delegate to the original method (e.g., .show()) if possible + if ( mode ) { + return this[ mode ]( args.duration, complete ); + } else { + return this.each( function() { + if ( complete ) { + complete.call( this ); + } + } ); + } + } + + function run( next ) { + var elem = $( this ); + + function cleanup() { + elem.removeData( dataSpaceAnimated ); + + $.effects.cleanUp( elem ); + + if ( args.mode === "hide" ) { + elem.hide(); + } + + done(); + } + + function done() { + if ( $.isFunction( complete ) ) { + complete.call( elem[ 0 ] ); + } + + if ( $.isFunction( next ) ) { + next(); + } + } + + // Override mode option on a per element basis, + // as toggle can be either show or hide depending on element state + args.mode = modes.shift(); + + if ( $.uiBackCompat !== false && !defaultMode ) { + if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { + + // Call the core method to track "olddisplay" properly + elem[ mode ](); + done(); + } else { + effectMethod.call( elem[ 0 ], args, done ); + } + } else { + if ( args.mode === "none" ) { + + // Call the core method to track "olddisplay" properly + elem[ mode ](); + done(); + } else { + effectMethod.call( elem[ 0 ], args, cleanup ); + } + } + } + + // Run prefilter on all elements first to ensure that + // any showing or hiding happens before placeholder creation, + // which ensures that any layout changes are correctly captured. + return queue === false ? + this.each( prefilter ).each( run ) : + this.queue( queueName, prefilter ).queue( queueName, run ); + }, + + show: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "show"; + return this.effect.call( this, args ); + } + }; + } )( $.fn.show ), + + hide: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "hide"; + return this.effect.call( this, args ); + } + }; + } )( $.fn.hide ), + + toggle: ( function( orig ) { + return function( option ) { + if ( standardAnimationOption( option ) || typeof option === "boolean" ) { + return orig.apply( this, arguments ); + } else { + var args = _normalizeArguments.apply( this, arguments ); + args.mode = "toggle"; + return this.effect.call( this, args ); + } + }; + } )( $.fn.toggle ), + + cssUnit: function( key ) { + var style = this.css( key ), + val = []; + + $.each( [ "em", "px", "%", "pt" ], function( i, unit ) { + if ( style.indexOf( unit ) > 0 ) { + val = [ parseFloat( style ), unit ]; + } + } ); + return val; + }, + + cssClip: function( clipObj ) { + if ( clipObj ) { + return this.css( "clip", "rect(" + clipObj.top + "px " + clipObj.right + "px " + + clipObj.bottom + "px " + clipObj.left + "px)" ); + } + return parseClip( this.css( "clip" ), this ); + }, + + transfer: function( options, done ) { + var element = $( this ), + target = $( options.to ), + targetFixed = target.css( "position" ) === "fixed", + body = $( "body" ), + fixTop = targetFixed ? body.scrollTop() : 0, + fixLeft = targetFixed ? body.scrollLeft() : 0, + endPosition = target.offset(), + animation = { + top: endPosition.top - fixTop, + left: endPosition.left - fixLeft, + height: target.innerHeight(), + width: target.innerWidth() + }, + startPosition = element.offset(), + transfer = $( "
    " ) + .appendTo( "body" ) + .addClass( options.className ) + .css( { + top: startPosition.top - fixTop, + left: startPosition.left - fixLeft, + height: element.innerHeight(), + width: element.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + } ) + .animate( animation, options.duration, options.easing, function() { + transfer.remove(); + if ( $.isFunction( done ) ) { + done(); + } + } ); + } +} ); + +function parseClip( str, element ) { + var outerWidth = element.outerWidth(), + outerHeight = element.outerHeight(), + clipRegex = /^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/, + values = clipRegex.exec( str ) || [ "", 0, outerWidth, outerHeight, 0 ]; + + return { + top: parseFloat( values[ 1 ] ) || 0, + right: values[ 2 ] === "auto" ? outerWidth : parseFloat( values[ 2 ] ), + bottom: values[ 3 ] === "auto" ? outerHeight : parseFloat( values[ 3 ] ), + left: parseFloat( values[ 4 ] ) || 0 + }; +} + +$.fx.step.clip = function( fx ) { + if ( !fx.clipInit ) { + fx.start = $( fx.elem ).cssClip(); + if ( typeof fx.end === "string" ) { + fx.end = parseClip( fx.end, fx.elem ); + } + fx.clipInit = true; + } + + $( fx.elem ).cssClip( { + top: fx.pos * ( fx.end.top - fx.start.top ) + fx.start.top, + right: fx.pos * ( fx.end.right - fx.start.right ) + fx.start.right, + bottom: fx.pos * ( fx.end.bottom - fx.start.bottom ) + fx.start.bottom, + left: fx.pos * ( fx.end.left - fx.start.left ) + fx.start.left + } ); +}; + +} )(); + +/******************************************************************************/ +/*********************************** EASING ***********************************/ +/******************************************************************************/ + +( function() { + +// Based on easing equations from Robert Penner (http://www.robertpenner.com/easing) + +var baseEasings = {}; + +$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) { + baseEasings[ name ] = function( p ) { + return Math.pow( p, i + 2 ); + }; +} ); + +$.extend( baseEasings, { + Sine: function( p ) { + return 1 - Math.cos( p * Math.PI / 2 ); + }, + Circ: function( p ) { + return 1 - Math.sqrt( 1 - p * p ); + }, + Elastic: function( p ) { + return p === 0 || p === 1 ? p : + -Math.pow( 2, 8 * ( p - 1 ) ) * Math.sin( ( ( p - 1 ) * 80 - 7.5 ) * Math.PI / 15 ); + }, + Back: function( p ) { + return p * p * ( 3 * p - 2 ); + }, + Bounce: function( p ) { + var pow2, + bounce = 4; + + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); + } +} ); + +$.each( baseEasings, function( name, easeIn ) { + $.easing[ "easeIn" + name ] = easeIn; + $.easing[ "easeOut" + name ] = function( p ) { + return 1 - easeIn( 1 - p ); + }; + $.easing[ "easeInOut" + name ] = function( p ) { + return p < 0.5 ? + easeIn( p * 2 ) / 2 : + 1 - easeIn( p * -2 + 2 ) / 2; + }; +} ); + +} )(); + +var effect = $.effects; + + +/*! + * jQuery UI Effects Blind 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Blind Effect +//>>group: Effects +//>>description: Blinds the element. +//>>docs: http://api.jqueryui.com/blind-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, done ) { + var map = { + up: [ "bottom", "top" ], + vertical: [ "bottom", "top" ], + down: [ "top", "bottom" ], + left: [ "right", "left" ], + horizontal: [ "right", "left" ], + right: [ "left", "right" ] + }, + element = $( this ), + direction = options.direction || "up", + start = element.cssClip(), + animate = { clip: $.extend( {}, start ) }, + placeholder = $.effects.createPlaceholder( element ); + + animate.clip[ map[ direction ][ 0 ] ] = animate.clip[ map[ direction ][ 1 ] ]; + + if ( options.mode === "show" ) { + element.cssClip( animate.clip ); + if ( placeholder ) { + placeholder.css( $.effects.clipToBox( animate ) ); + } + + animate.clip = start; + } + + if ( placeholder ) { + placeholder.animate( $.effects.clipToBox( animate ), options.duration, options.easing ); + } + + element.animate( animate, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); + + +/*! + * jQuery UI Effects Bounce 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Bounce Effect +//>>group: Effects +//>>description: Bounces an element horizontally or vertically n times. +//>>docs: http://api.jqueryui.com/bounce-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) { + var upAnim, downAnim, refValue, + element = $( this ), + + // Defaults: + mode = options.mode, + hide = mode === "hide", + show = mode === "show", + direction = options.direction || "up", + distance = options.distance, + times = options.times || 5, + + // Number of internal animations + anims = times * 2 + ( show || hide ? 1 : 0 ), + speed = options.duration / anims, + easing = options.easing, + + // Utility: + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ), + i = 0, + + queuelen = element.queue().length; + + $.effects.createPlaceholder( element ); + + refValue = element.css( ref ); + + // Default distance for the BIGGEST bounce is the outer Distance / 3 + if ( !distance ) { + distance = element[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3; + } + + if ( show ) { + downAnim = { opacity: 1 }; + downAnim[ ref ] = refValue; + + // If we are showing, force opacity 0 and set the initial position + // then do the "first" animation + element + .css( "opacity", 0 ) + .css( ref, motion ? -distance * 2 : distance * 2 ) + .animate( downAnim, speed, easing ); + } + + // Start at the smallest distance if we are hiding + if ( hide ) { + distance = distance / Math.pow( 2, times - 1 ); + } + + downAnim = {}; + downAnim[ ref ] = refValue; + + // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here + for ( ; i < times; i++ ) { + upAnim = {}; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + element + .animate( upAnim, speed, easing ) + .animate( downAnim, speed, easing ); + + distance = hide ? distance * 2 : distance / 2; + } + + // Last Bounce when Hiding + if ( hide ) { + upAnim = { opacity: 0 }; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + element.animate( upAnim, speed, easing ); + } + + element.queue( done ); + + $.effects.unshift( element, queuelen, anims + 1 ); +} ); + + +/*! + * jQuery UI Effects Clip 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Clip Effect +//>>group: Effects +//>>description: Clips the element on and off like an old TV. +//>>docs: http://api.jqueryui.com/clip-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectClip = $.effects.define( "clip", "hide", function( options, done ) { + var start, + animate = {}, + element = $( this ), + direction = options.direction || "vertical", + both = direction === "both", + horizontal = both || direction === "horizontal", + vertical = both || direction === "vertical"; + + start = element.cssClip(); + animate.clip = { + top: vertical ? ( start.bottom - start.top ) / 2 : start.top, + right: horizontal ? ( start.right - start.left ) / 2 : start.right, + bottom: vertical ? ( start.bottom - start.top ) / 2 : start.bottom, + left: horizontal ? ( start.right - start.left ) / 2 : start.left + }; + + $.effects.createPlaceholder( element ); + + if ( options.mode === "show" ) { + element.cssClip( animate.clip ); + animate.clip = start; + } + + element.animate( animate, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); + +} ); + + +/*! + * jQuery UI Effects Drop 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Drop Effect +//>>group: Effects +//>>description: Moves an element in one direction and hides it at the same time. +//>>docs: http://api.jqueryui.com/drop-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, done ) { + + var distance, + element = $( this ), + mode = options.mode, + show = mode === "show", + direction = options.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ) ? "-=" : "+=", + oppositeMotion = ( motion === "+=" ) ? "-=" : "+=", + animation = { + opacity: 0 + }; + + $.effects.createPlaceholder( element ); + + distance = options.distance || + element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ) / 2; + + animation[ ref ] = motion + distance; + + if ( show ) { + element.css( animation ); + + animation[ ref ] = oppositeMotion + distance; + animation.opacity = 1; + } + + // Animate + element.animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); + + +/*! + * jQuery UI Effects Explode 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Explode Effect +//>>group: Effects +// jscs:disable maximumLineLength +//>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness. +// jscs:enable maximumLineLength +//>>docs: http://api.jqueryui.com/explode-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectExplode = $.effects.define( "explode", "hide", function( options, done ) { + + var i, j, left, top, mx, my, + rows = options.pieces ? Math.round( Math.sqrt( options.pieces ) ) : 3, + cells = rows, + element = $( this ), + mode = options.mode, + show = mode === "show", + + // Show and then visibility:hidden the element before calculating offset + offset = element.show().css( "visibility", "hidden" ).offset(), + + // Width and height of a piece + width = Math.ceil( element.outerWidth() / cells ), + height = Math.ceil( element.outerHeight() / rows ), + pieces = []; + + // Children animate complete: + function childComplete() { + pieces.push( this ); + if ( pieces.length === rows * cells ) { + animComplete(); + } + } + + // Clone the element for each row and cell. + for ( i = 0; i < rows; i++ ) { // ===> + top = offset.top + i * height; + my = i - ( rows - 1 ) / 2; + + for ( j = 0; j < cells; j++ ) { // ||| + left = offset.left + j * width; + mx = j - ( cells - 1 ) / 2; + + // Create a clone of the now hidden main element that will be absolute positioned + // within a wrapper div off the -left and -top equal to size of our pieces + element + .clone() + .appendTo( "body" ) + .wrap( "
    " ) + .css( { + position: "absolute", + visibility: "visible", + left: -j * width, + top: -i * height + } ) + + // Select the wrapper - make it overflow: hidden and absolute positioned based on + // where the original was located +left and +top equal to the size of pieces + .parent() + .addClass( "ui-effects-explode" ) + .css( { + position: "absolute", + overflow: "hidden", + width: width, + height: height, + left: left + ( show ? mx * width : 0 ), + top: top + ( show ? my * height : 0 ), + opacity: show ? 0 : 1 + } ) + .animate( { + left: left + ( show ? 0 : mx * width ), + top: top + ( show ? 0 : my * height ), + opacity: show ? 1 : 0 + }, options.duration || 500, options.easing, childComplete ); + } + } + + function animComplete() { + element.css( { + visibility: "visible" + } ); + $( pieces ).remove(); + done(); + } +} ); + + +/*! + * jQuery UI Effects Fade 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Fade Effect +//>>group: Effects +//>>description: Fades the element. +//>>docs: http://api.jqueryui.com/fade-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, done ) { + var show = options.mode === "show"; + + $( this ) + .css( "opacity", show ? 0 : 1 ) + .animate( { + opacity: show ? 1 : 0 + }, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); + + +/*! + * jQuery UI Effects Fold 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Fold Effect +//>>group: Effects +//>>description: Folds an element first horizontally and then vertically. +//>>docs: http://api.jqueryui.com/fold-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectFold = $.effects.define( "fold", "hide", function( options, done ) { + + // Create element + var element = $( this ), + mode = options.mode, + show = mode === "show", + hide = mode === "hide", + size = options.size || 15, + percent = /([0-9]+)%/.exec( size ), + horizFirst = !!options.horizFirst, + ref = horizFirst ? [ "right", "bottom" ] : [ "bottom", "right" ], + duration = options.duration / 2, + + placeholder = $.effects.createPlaceholder( element ), + + start = element.cssClip(), + animation1 = { clip: $.extend( {}, start ) }, + animation2 = { clip: $.extend( {}, start ) }, + + distance = [ start[ ref[ 0 ] ], start[ ref[ 1 ] ] ], + + queuelen = element.queue().length; + + if ( percent ) { + size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ]; + } + animation1.clip[ ref[ 0 ] ] = size; + animation2.clip[ ref[ 0 ] ] = size; + animation2.clip[ ref[ 1 ] ] = 0; + + if ( show ) { + element.cssClip( animation2.clip ); + if ( placeholder ) { + placeholder.css( $.effects.clipToBox( animation2 ) ); + } + + animation2.clip = start; + } + + // Animate + element + .queue( function( next ) { + if ( placeholder ) { + placeholder + .animate( $.effects.clipToBox( animation1 ), duration, options.easing ) + .animate( $.effects.clipToBox( animation2 ), duration, options.easing ); + } + + next(); + } ) + .animate( animation1, duration, options.easing ) + .animate( animation2, duration, options.easing ) + .queue( done ); + + $.effects.unshift( element, queuelen, 4 ); +} ); + + +/*! + * jQuery UI Effects Highlight 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Highlight Effect +//>>group: Effects +//>>description: Highlights the background of an element in a defined color for a custom duration. +//>>docs: http://api.jqueryui.com/highlight-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectHighlight = $.effects.define( "highlight", "show", function( options, done ) { + var element = $( this ), + animation = { + backgroundColor: element.css( "backgroundColor" ) + }; + + if ( options.mode === "hide" ) { + animation.opacity = 0; + } + + $.effects.saveStyle( element ); + + element + .css( { + backgroundImage: "none", + backgroundColor: options.color || "#ffff99" + } ) + .animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); + + +/*! + * jQuery UI Effects Size 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Size Effect +//>>group: Effects +//>>description: Resize an element to a specified width and height. +//>>docs: http://api.jqueryui.com/size-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectSize = $.effects.define( "size", function( options, done ) { + + // Create element + var baseline, factor, temp, + element = $( this ), + + // Copy for children + cProps = [ "fontSize" ], + vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ], + hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ], + + // Set options + mode = options.mode, + restore = mode !== "effect", + scale = options.scale || "both", + origin = options.origin || [ "middle", "center" ], + position = element.css( "position" ), + pos = element.position(), + original = $.effects.scaledDimensions( element ), + from = options.from || original, + to = options.to || $.effects.scaledDimensions( element, 0 ); + + $.effects.createPlaceholder( element ); + + if ( mode === "show" ) { + temp = from; + from = to; + to = temp; + } + + // Set scaling factor + factor = { + from: { + y: from.height / original.height, + x: from.width / original.width + }, + to: { + y: to.height / original.height, + x: to.width / original.width + } + }; + + // Scale the css box + if ( scale === "box" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + from = $.effects.setTransition( element, vProps, factor.from.y, from ); + to = $.effects.setTransition( element, vProps, factor.to.y, to ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + from = $.effects.setTransition( element, hProps, factor.from.x, from ); + to = $.effects.setTransition( element, hProps, factor.to.x, to ); + } + } + + // Scale the content + if ( scale === "content" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + from = $.effects.setTransition( element, cProps, factor.from.y, from ); + to = $.effects.setTransition( element, cProps, factor.to.y, to ); + } + } + + // Adjust the position properties based on the provided origin points + if ( origin ) { + baseline = $.effects.getBaseline( origin, original ); + from.top = ( original.outerHeight - from.outerHeight ) * baseline.y + pos.top; + from.left = ( original.outerWidth - from.outerWidth ) * baseline.x + pos.left; + to.top = ( original.outerHeight - to.outerHeight ) * baseline.y + pos.top; + to.left = ( original.outerWidth - to.outerWidth ) * baseline.x + pos.left; + } + element.css( from ); + + // Animate the children if desired + if ( scale === "content" || scale === "both" ) { + + vProps = vProps.concat( [ "marginTop", "marginBottom" ] ).concat( cProps ); + hProps = hProps.concat( [ "marginLeft", "marginRight" ] ); + + // Only animate children with width attributes specified + // TODO: is this right? should we include anything with css width specified as well + element.find( "*[width]" ).each( function() { + var child = $( this ), + childOriginal = $.effects.scaledDimensions( child ), + childFrom = { + height: childOriginal.height * factor.from.y, + width: childOriginal.width * factor.from.x, + outerHeight: childOriginal.outerHeight * factor.from.y, + outerWidth: childOriginal.outerWidth * factor.from.x + }, + childTo = { + height: childOriginal.height * factor.to.y, + width: childOriginal.width * factor.to.x, + outerHeight: childOriginal.height * factor.to.y, + outerWidth: childOriginal.width * factor.to.x + }; + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + childFrom = $.effects.setTransition( child, vProps, factor.from.y, childFrom ); + childTo = $.effects.setTransition( child, vProps, factor.to.y, childTo ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + childFrom = $.effects.setTransition( child, hProps, factor.from.x, childFrom ); + childTo = $.effects.setTransition( child, hProps, factor.to.x, childTo ); + } + + if ( restore ) { + $.effects.saveStyle( child ); + } + + // Animate children + child.css( childFrom ); + child.animate( childTo, options.duration, options.easing, function() { + + // Restore children + if ( restore ) { + $.effects.restoreStyle( child ); + } + } ); + } ); + } + + // Animate + element.animate( to, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: function() { + + var offset = element.offset(); + + if ( to.opacity === 0 ) { + element.css( "opacity", from.opacity ); + } + + if ( !restore ) { + element + .css( "position", position === "static" ? "relative" : position ) + .offset( offset ); + + // Need to save style here so that automatic style restoration + // doesn't restore to the original styles from before the animation. + $.effects.saveStyle( element ); + } + + done(); + } + } ); + +} ); + + +/*! + * jQuery UI Effects Scale 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Scale Effect +//>>group: Effects +//>>description: Grows or shrinks an element and its content. +//>>docs: http://api.jqueryui.com/scale-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectScale = $.effects.define( "scale", function( options, done ) { + + // Create element + var el = $( this ), + mode = options.mode, + percent = parseInt( options.percent, 10 ) || + ( parseInt( options.percent, 10 ) === 0 ? 0 : ( mode !== "effect" ? 0 : 100 ) ), + + newOptions = $.extend( true, { + from: $.effects.scaledDimensions( el ), + to: $.effects.scaledDimensions( el, percent, options.direction || "both" ), + origin: options.origin || [ "middle", "center" ] + }, options ); + + // Fade option to support puff + if ( options.fade ) { + newOptions.from.opacity = 1; + newOptions.to.opacity = 0; + } + + $.effects.effect.size.call( this, newOptions, done ); +} ); + + +/*! + * jQuery UI Effects Puff 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Puff Effect +//>>group: Effects +//>>description: Creates a puff effect by scaling the element up and hiding it at the same time. +//>>docs: http://api.jqueryui.com/puff-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, done ) { + var newOptions = $.extend( true, {}, options, { + fade: true, + percent: parseInt( options.percent, 10 ) || 150 + } ); + + $.effects.effect.scale.call( this, newOptions, done ); +} ); + + +/*! + * jQuery UI Effects Pulsate 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Pulsate Effect +//>>group: Effects +//>>description: Pulsates an element n times by changing the opacity to zero and back. +//>>docs: http://api.jqueryui.com/pulsate-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( options, done ) { + var element = $( this ), + mode = options.mode, + show = mode === "show", + hide = mode === "hide", + showhide = show || hide, + + // Showing or hiding leaves off the "last" animation + anims = ( ( options.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ), + duration = options.duration / anims, + animateTo = 0, + i = 1, + queuelen = element.queue().length; + + if ( show || !element.is( ":visible" ) ) { + element.css( "opacity", 0 ).show(); + animateTo = 1; + } + + // Anims - 1 opacity "toggles" + for ( ; i < anims; i++ ) { + element.animate( { opacity: animateTo }, duration, options.easing ); + animateTo = 1 - animateTo; + } + + element.animate( { opacity: animateTo }, duration, options.easing ); + + element.queue( done ); + + $.effects.unshift( element, queuelen, anims + 1 ); +} ); + + +/*! + * jQuery UI Effects Shake 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Shake Effect +//>>group: Effects +//>>description: Shakes an element horizontally or vertically n times. +//>>docs: http://api.jqueryui.com/shake-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectShake = $.effects.define( "shake", function( options, done ) { + + var i = 1, + element = $( this ), + direction = options.direction || "left", + distance = options.distance || 20, + times = options.times || 3, + anims = times * 2 + 1, + speed = Math.round( options.duration / anims ), + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + positiveMotion = ( direction === "up" || direction === "left" ), + animation = {}, + animation1 = {}, + animation2 = {}, + + queuelen = element.queue().length; + + $.effects.createPlaceholder( element ); + + // Animation + animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance; + animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2; + animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2; + + // Animate + element.animate( animation, speed, options.easing ); + + // Shakes + for ( ; i < times; i++ ) { + element + .animate( animation1, speed, options.easing ) + .animate( animation2, speed, options.easing ); + } + + element + .animate( animation1, speed, options.easing ) + .animate( animation, speed / 2, options.easing ) + .queue( done ); + + $.effects.unshift( element, queuelen, anims + 1 ); +} ); + + +/*! + * jQuery UI Effects Slide 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Slide Effect +//>>group: Effects +//>>description: Slides an element in and out of the viewport. +//>>docs: http://api.jqueryui.com/slide-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effectsEffectSlide = $.effects.define( "slide", "show", function( options, done ) { + var startClip, startRef, + element = $( this ), + map = { + up: [ "bottom", "top" ], + down: [ "top", "bottom" ], + left: [ "right", "left" ], + right: [ "left", "right" ] + }, + mode = options.mode, + direction = options.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + positiveMotion = ( direction === "up" || direction === "left" ), + distance = options.distance || + element[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ), + animation = {}; + + $.effects.createPlaceholder( element ); + + startClip = element.cssClip(); + startRef = element.position()[ ref ]; + + // Define hide animation + animation[ ref ] = ( positiveMotion ? -1 : 1 ) * distance + startRef; + animation.clip = element.cssClip(); + animation.clip[ map[ direction ][ 1 ] ] = animation.clip[ map[ direction ][ 0 ] ]; + + // Reverse the animation if we're showing + if ( mode === "show" ) { + element.cssClip( animation.clip ); + element.css( ref, animation[ ref ] ); + animation.clip = startClip; + animation[ ref ] = startRef; + } + + // Actually animate + element.animate( animation, { + queue: false, + duration: options.duration, + easing: options.easing, + complete: done + } ); +} ); + + +/*! + * jQuery UI Effects Transfer 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Transfer Effect +//>>group: Effects +//>>description: Displays a transfer effect from one element to another. +//>>docs: http://api.jqueryui.com/transfer-effect/ +//>>demos: http://jqueryui.com/effect/ + + + +var effect; +if ( $.uiBackCompat !== false ) { + effect = $.effects.define( "transfer", function( options, done ) { + $( this ).transfer( options, done ); + } ); +} +var effectsEffectTransfer = effect; + + +/*! + * jQuery UI Focusable 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :focusable Selector +//>>group: Core +//>>description: Selects elements which can be focused. +//>>docs: http://api.jqueryui.com/focusable-selector/ + + + +// Selectors +$.ui.focusable = function( element, hasTabindex ) { + var map, mapName, img, focusableIfVisible, fieldset, + nodeName = element.nodeName.toLowerCase(); + + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap='#" + mapName + "']" ); + return img.length > 0 && img.is( ":visible" ); + } + + if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) { + focusableIfVisible = !element.disabled; + + if ( focusableIfVisible ) { + + // Form controls within a disabled fieldset are disabled. + // However, controls within the fieldset's legend do not get disabled. + // Since controls generally aren't placed inside legends, we skip + // this portion of the check. + fieldset = $( element ).closest( "fieldset" )[ 0 ]; + if ( fieldset ) { + focusableIfVisible = !fieldset.disabled; + } + } + } else if ( "a" === nodeName ) { + focusableIfVisible = element.href || hasTabindex; + } else { + focusableIfVisible = hasTabindex; + } + + return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); +}; + +// Support: IE 8 only +// IE 8 doesn't resolve inherit to visible/hidden for computed values +function visible( element ) { + var visibility = element.css( "visibility" ); + while ( visibility === "inherit" ) { + element = element.parent(); + visibility = element.css( "visibility" ); + } + return visibility !== "hidden"; +} + +$.extend( $.expr[ ":" ], { + focusable: function( element ) { + return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); + } +} ); + +var focusable = $.ui.focusable; + + + + +// Support: IE8 Only +// IE8 does not support the form attribute and when it is supplied. It overwrites the form prop +// with a string, so we need to find the proper form. +var form = $.fn.form = function() { + return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); +}; + + +/*! + * jQuery UI Form Reset Mixin 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Form Reset Mixin +//>>group: Core +//>>description: Refresh input widgets when their form is reset +//>>docs: http://api.jqueryui.com/form-reset-mixin/ + + + +var formResetMixin = $.ui.formResetMixin = { + _formResetHandler: function() { + var form = $( this ); + + // Wait for the form reset to actually happen before refreshing + setTimeout( function() { + var instances = form.data( "ui-form-reset-instances" ); + $.each( instances, function() { + this.refresh(); + } ); + } ); + }, + + _bindFormResetHandler: function() { + this.form = this.element.form(); + if ( !this.form.length ) { + return; + } + + var instances = this.form.data( "ui-form-reset-instances" ) || []; + if ( !instances.length ) { + + // We don't use _on() here because we use a single event handler per form + this.form.on( "reset.ui-form-reset", this._formResetHandler ); + } + instances.push( this ); + this.form.data( "ui-form-reset-instances", instances ); + }, + + _unbindFormResetHandler: function() { + if ( !this.form.length ) { + return; + } + + var instances = this.form.data( "ui-form-reset-instances" ); + instances.splice( $.inArray( this, instances ), 1 ); + if ( instances.length ) { + this.form.data( "ui-form-reset-instances", instances ); + } else { + this.form + .removeData( "ui-form-reset-instances" ) + .off( "reset.ui-form-reset" ); + } + } +}; + + +/*! + * jQuery UI Support for jQuery core 1.7.x 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + */ + +//>>label: jQuery 1.7 Support +//>>group: Core +//>>description: Support version 1.7.x of jQuery core + + + +// Support: jQuery 1.7 only +// Not a great way to check versions, but since we only support 1.7+ and only +// need to detect <1.8, this is a simple check that should suffice. Checking +// for "1.7." would be a bit safer, but the version string is 1.7, not 1.7.0 +// and we'll never reach 1.70.0 (if we do, we certainly won't be supporting +// 1.7 anymore). See #11197 for why we're not using feature detection. +if ( $.fn.jquery.substring( 0, 3 ) === "1.7" ) { + + // Setters for .innerWidth(), .innerHeight(), .outerWidth(), .outerHeight() + // Unlike jQuery Core 1.8+, these only support numeric values to set the + // dimensions in pixels + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + } ); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each( function() { + $( this ).css( type, reduce( this, size ) + "px" ); + } ); + }; + + $.fn[ "outer" + name ] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each( function() { + $( this ).css( type, reduce( this, size, true, margin ) + "px" ); + } ); + }; + } ); + + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +; +/*! + * jQuery UI Keycode 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Keycode +//>>group: Core +//>>description: Provide keycodes as keynames +//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ + + +var keycode = $.ui.keyCode = { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 +}; + + + + +// Internal use only +var escapeSelector = $.ui.escapeSelector = ( function() { + var selectorEscape = /([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g; + return function( selector ) { + return selector.replace( selectorEscape, "\\$1" ); + }; +} )(); + + +/*! + * jQuery UI Labels 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: labels +//>>group: Core +//>>description: Find all the labels associated with a given input +//>>docs: http://api.jqueryui.com/labels/ + + + +var labels = $.fn.labels = function() { + var ancestor, selector, id, labels, ancestors; + + // Check control.labels first + if ( this[ 0 ].labels && this[ 0 ].labels.length ) { + return this.pushStack( this[ 0 ].labels ); + } + + // Support: IE <= 11, FF <= 37, Android <= 2.3 only + // Above browsers do not support control.labels. Everything below is to support them + // as well as document fragments. control.labels does not work on document fragments + labels = this.eq( 0 ).parents( "label" ); + + // Look for the label based on the id + id = this.attr( "id" ); + if ( id ) { + + // We don't search against the document in case the element + // is disconnected from the DOM + ancestor = this.eq( 0 ).parents().last(); + + // Get a full set of top level ancestors + ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); + + // Create a selector for the label based on the id + selector = "label[for='" + $.ui.escapeSelector( id ) + "']"; + + labels = labels.add( ancestors.find( selector ).addBack( selector ) ); + + } + + // Return whatever we have found for labels + return this.pushStack( labels ); +}; + + +/*! + * jQuery UI Scroll Parent 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: scrollParent +//>>group: Core +//>>description: Get the closest ancestor element that is scrollable. +//>>docs: http://api.jqueryui.com/scrollParent/ + + + +var scrollParent = $.fn.scrollParent = function( includeHidden ) { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; + } + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + + parent.css( "overflow-x" ) ); + } ).eq( 0 ); + + return position === "fixed" || !scrollParent.length ? + $( this[ 0 ].ownerDocument || document ) : + scrollParent; +}; + + +/*! + * jQuery UI Tabbable 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :tabbable Selector +//>>group: Core +//>>description: Selects elements which can be tabbed to. +//>>docs: http://api.jqueryui.com/tabbable-selector/ + + + +var tabbable = $.extend( $.expr[ ":" ], { + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + hasTabindex = tabIndex != null; + return ( !hasTabindex || tabIndex >= 0 ) && $.ui.focusable( element, hasTabindex ); + } +} ); + + +/*! + * jQuery UI Unique ID 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: uniqueId +//>>group: Core +//>>description: Functions to generate and remove uniqueId's +//>>docs: http://api.jqueryui.com/uniqueId/ + + + +var uniqueId = $.fn.extend( { + uniqueId: ( function() { + var uuid = 0; + + return function() { + return this.each( function() { + if ( !this.id ) { + this.id = "ui-id-" + ( ++uuid ); + } + } ); + }; + } )(), + + removeUniqueId: function() { + return this.each( function() { + if ( /^ui-id-\d+$/.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + } ); + } +} ); + + +/*! + * jQuery UI Accordion 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Accordion +//>>group: Widgets +// jscs:disable maximumLineLength +//>>description: Displays collapsible content panels for presenting information in a limited amount of space. +// jscs:enable maximumLineLength +//>>docs: http://api.jqueryui.com/accordion/ +//>>demos: http://jqueryui.com/accordion/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/accordion.css +//>>css.theme: ../../themes/base/theme.css + + + +var widgetsAccordion = $.widget( "ui.accordion", { + version: "1.12.1", + options: { + active: 0, + animate: {}, + classes: { + "ui-accordion-header": "ui-corner-top", + "ui-accordion-header-collapsed": "ui-corner-all", + "ui-accordion-content": "ui-corner-bottom" + }, + collapsible: false, + event: "click", + header: "> li > :first-child, > :not(li):even", + heightStyle: "auto", + icons: { + activeHeader: "ui-icon-triangle-1-s", + header: "ui-icon-triangle-1-e" + }, + + // Callbacks + activate: null, + beforeActivate: null + }, + + hideProps: { + borderTopWidth: "hide", + borderBottomWidth: "hide", + paddingTop: "hide", + paddingBottom: "hide", + height: "hide" + }, + + showProps: { + borderTopWidth: "show", + borderBottomWidth: "show", + paddingTop: "show", + paddingBottom: "show", + height: "show" + }, + + _create: function() { + var options = this.options; + + this.prevShow = this.prevHide = $(); + this._addClass( "ui-accordion", "ui-widget ui-helper-reset" ); + this.element.attr( "role", "tablist" ); + + // Don't allow collapsible: false and active: false / null + if ( !options.collapsible && ( options.active === false || options.active == null ) ) { + options.active = 0; + } + + this._processPanels(); + + // handle negative values + if ( options.active < 0 ) { + options.active += this.headers.length; + } + this._refresh(); + }, + + _getCreateEventData: function() { + return { + header: this.active, + panel: !this.active.length ? $() : this.active.next() + }; + }, + + _createIcons: function() { + var icon, children, + icons = this.options.icons; + + if ( icons ) { + icon = $( "" ); + this._addClass( icon, "ui-accordion-header-icon", "ui-icon " + icons.header ); + icon.prependTo( this.headers ); + children = this.active.children( ".ui-accordion-header-icon" ); + this._removeClass( children, icons.header ) + ._addClass( children, null, icons.activeHeader ) + ._addClass( this.headers, "ui-accordion-icons" ); + } + }, + + _destroyIcons: function() { + this._removeClass( this.headers, "ui-accordion-icons" ); + this.headers.children( ".ui-accordion-header-icon" ).remove(); + }, + + _destroy: function() { + var contents; + + // Clean up main element + this.element.removeAttr( "role" ); + + // Clean up headers + this.headers + .removeAttr( "role aria-expanded aria-selected aria-controls tabIndex" ) + .removeUniqueId(); + + this._destroyIcons(); + + // Clean up content panels + contents = this.headers.next() + .css( "display", "" ) + .removeAttr( "role aria-hidden aria-labelledby" ) + .removeUniqueId(); + + if ( this.options.heightStyle !== "content" ) { + contents.css( "height", "" ); + } + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "event" ) { + if ( this.options.event ) { + this._off( this.headers, this.options.event ); + } + this._setupEvents( value ); + } + + this._super( key, value ); + + // Setting collapsible: false while collapsed; open first panel + if ( key === "collapsible" && !value && this.options.active === false ) { + this._activate( 0 ); + } + + if ( key === "icons" ) { + this._destroyIcons(); + if ( value ) { + this._createIcons(); + } + } + }, + + _setOptionDisabled: function( value ) { + this._super( value ); + + this.element.attr( "aria-disabled", value ); + + // Support: IE8 Only + // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE + // so we need to add the disabled class to the headers and panels + this._toggleClass( null, "ui-state-disabled", !!value ); + this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", + !!value ); + }, + + _keydown: function( event ) { + if ( event.altKey || event.ctrlKey ) { + return; + } + + var keyCode = $.ui.keyCode, + length = this.headers.length, + currentIndex = this.headers.index( event.target ), + toFocus = false; + + switch ( event.keyCode ) { + case keyCode.RIGHT: + case keyCode.DOWN: + toFocus = this.headers[ ( currentIndex + 1 ) % length ]; + break; + case keyCode.LEFT: + case keyCode.UP: + toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; + break; + case keyCode.SPACE: + case keyCode.ENTER: + this._eventHandler( event ); + break; + case keyCode.HOME: + toFocus = this.headers[ 0 ]; + break; + case keyCode.END: + toFocus = this.headers[ length - 1 ]; + break; + } + + if ( toFocus ) { + $( event.target ).attr( "tabIndex", -1 ); + $( toFocus ).attr( "tabIndex", 0 ); + $( toFocus ).trigger( "focus" ); + event.preventDefault(); + } + }, + + _panelKeyDown: function( event ) { + if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + $( event.currentTarget ).prev().trigger( "focus" ); + } + }, + + refresh: function() { + var options = this.options; + this._processPanels(); + + // Was collapsed or no panel + if ( ( options.active === false && options.collapsible === true ) || + !this.headers.length ) { + options.active = false; + this.active = $(); + + // active false only when collapsible is true + } else if ( options.active === false ) { + this._activate( 0 ); + + // was active, but active panel is gone + } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + + // all remaining panel are disabled + if ( this.headers.length === this.headers.find( ".ui-state-disabled" ).length ) { + options.active = false; + this.active = $(); + + // activate previous panel + } else { + this._activate( Math.max( 0, options.active - 1 ) ); + } + + // was active, active panel still exists + } else { + + // make sure active index is correct + options.active = this.headers.index( this.active ); + } + + this._destroyIcons(); + + this._refresh(); + }, + + _processPanels: function() { + var prevHeaders = this.headers, + prevPanels = this.panels; + + this.headers = this.element.find( this.options.header ); + this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", + "ui-state-default" ); + + this.panels = this.headers.next().filter( ":not(.ui-accordion-content-active)" ).hide(); + this._addClass( this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content" ); + + // Avoid memory leaks (#10056) + if ( prevPanels ) { + this._off( prevHeaders.not( this.headers ) ); + this._off( prevPanels.not( this.panels ) ); + } + }, + + _refresh: function() { + var maxHeight, + options = this.options, + heightStyle = options.heightStyle, + parent = this.element.parent(); + + this.active = this._findActive( options.active ); + this._addClass( this.active, "ui-accordion-header-active", "ui-state-active" ) + ._removeClass( this.active, "ui-accordion-header-collapsed" ); + this._addClass( this.active.next(), "ui-accordion-content-active" ); + this.active.next().show(); + + this.headers + .attr( "role", "tab" ) + .each( function() { + var header = $( this ), + headerId = header.uniqueId().attr( "id" ), + panel = header.next(), + panelId = panel.uniqueId().attr( "id" ); + header.attr( "aria-controls", panelId ); + panel.attr( "aria-labelledby", headerId ); + } ) + .next() + .attr( "role", "tabpanel" ); + + this.headers + .not( this.active ) + .attr( { + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + } ) + .next() + .attr( { + "aria-hidden": "true" + } ) + .hide(); + + // Make sure at least one header is in the tab order + if ( !this.active.length ) { + this.headers.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active.attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ) + .next() + .attr( { + "aria-hidden": "false" + } ); + } + + this._createIcons(); + + this._setupEvents( options.event ); + + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + this.element.siblings( ":visible" ).each( function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + } ); + + this.headers.each( function() { + maxHeight -= $( this ).outerHeight( true ); + } ); + + this.headers.next() + .each( function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + } ) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.headers.next() + .each( function() { + var isVisible = $( this ).is( ":visible" ); + if ( !isVisible ) { + $( this ).show(); + } + maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); + if ( !isVisible ) { + $( this ).hide(); + } + } ) + .height( maxHeight ); + } + }, + + _activate: function( index ) { + var active = this._findActive( index )[ 0 ]; + + // Trying to activate the already active panel + if ( active === this.active[ 0 ] ) { + return; + } + + // Trying to collapse, simulate a click on the currently active header + active = active || this.active[ 0 ]; + + this._eventHandler( { + target: active, + currentTarget: active, + preventDefault: $.noop + } ); + }, + + _findActive: function( selector ) { + return typeof selector === "number" ? this.headers.eq( selector ) : $(); + }, + + _setupEvents: function( event ) { + var events = { + keydown: "_keydown" + }; + if ( event ) { + $.each( event.split( " " ), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + } ); + } + + this._off( this.headers.add( this.headers.next() ) ); + this._on( this.headers, events ); + this._on( this.headers.next(), { keydown: "_panelKeyDown" } ); + this._hoverable( this.headers ); + this._focusable( this.headers ); + }, + + _eventHandler: function( event ) { + var activeChildren, clickedChildren, + options = this.options, + active = this.active, + clicked = $( event.currentTarget ), + clickedIsActive = clicked[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : clicked.next(), + toHide = active.next(), + eventData = { + oldHeader: active, + oldPanel: toHide, + newHeader: collapsing ? $() : clicked, + newPanel: toShow + }; + + event.preventDefault(); + + if ( + + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.headers.index( clicked ); + + // When the call to ._toggle() comes after the class changes + // it causes a very odd bug in IE 8 (see #6720) + this.active = clickedIsActive ? $() : clicked; + this._toggle( eventData ); + + // Switch classes + // corner classes on the previously active header stay after the animation + this._removeClass( active, "ui-accordion-header-active", "ui-state-active" ); + if ( options.icons ) { + activeChildren = active.children( ".ui-accordion-header-icon" ); + this._removeClass( activeChildren, null, options.icons.activeHeader ) + ._addClass( activeChildren, null, options.icons.header ); + } + + if ( !clickedIsActive ) { + this._removeClass( clicked, "ui-accordion-header-collapsed" ) + ._addClass( clicked, "ui-accordion-header-active", "ui-state-active" ); + if ( options.icons ) { + clickedChildren = clicked.children( ".ui-accordion-header-icon" ); + this._removeClass( clickedChildren, null, options.icons.header ) + ._addClass( clickedChildren, null, options.icons.activeHeader ); + } + + this._addClass( clicked.next(), "ui-accordion-content-active" ); + } + }, + + _toggle: function( data ) { + var toShow = data.newPanel, + toHide = this.prevShow.length ? this.prevShow : data.oldPanel; + + // Handle activating a panel during the animation for another activation + this.prevShow.add( this.prevHide ).stop( true, true ); + this.prevShow = toShow; + this.prevHide = toHide; + + if ( this.options.animate ) { + this._animate( toShow, toHide, data ); + } else { + toHide.hide(); + toShow.show(); + this._toggleComplete( data ); + } + + toHide.attr( { + "aria-hidden": "true" + } ); + toHide.prev().attr( { + "aria-selected": "false", + "aria-expanded": "false" + } ); + + // if we're switching panels, remove the old header from the tab order + // if we're opening from collapsed state, remove the previous header from the tab order + // if we're collapsing, then keep the collapsing header in the tab order + if ( toShow.length && toHide.length ) { + toHide.prev().attr( { + "tabIndex": -1, + "aria-expanded": "false" + } ); + } else if ( toShow.length ) { + this.headers.filter( function() { + return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; + } ) + .attr( "tabIndex", -1 ); + } + + toShow + .attr( "aria-hidden", "false" ) + .prev() + .attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + }, + + _animate: function( toShow, toHide, data ) { + var total, easing, duration, + that = this, + adjust = 0, + boxSizing = toShow.css( "box-sizing" ), + down = toShow.length && + ( !toHide.length || ( toShow.index() < toHide.index() ) ), + animate = this.options.animate || {}, + options = down && animate.down || animate, + complete = function() { + that._toggleComplete( data ); + }; + + if ( typeof options === "number" ) { + duration = options; + } + if ( typeof options === "string" ) { + easing = options; + } + + // fall back from options to animation in case of partial down settings + easing = easing || options.easing || animate.easing; + duration = duration || options.duration || animate.duration; + + if ( !toHide.length ) { + return toShow.animate( this.showProps, duration, easing, complete ); + } + if ( !toShow.length ) { + return toHide.animate( this.hideProps, duration, easing, complete ); + } + + total = toShow.show().outerHeight(); + toHide.animate( this.hideProps, { + duration: duration, + easing: easing, + step: function( now, fx ) { + fx.now = Math.round( now ); + } + } ); + toShow + .hide() + .animate( this.showProps, { + duration: duration, + easing: easing, + complete: complete, + step: function( now, fx ) { + fx.now = Math.round( now ); + if ( fx.prop !== "height" ) { + if ( boxSizing === "content-box" ) { + adjust += fx.now; + } + } else if ( that.options.heightStyle !== "content" ) { + fx.now = Math.round( total - toHide.outerHeight() - adjust ); + adjust = 0; + } + } + } ); + }, + + _toggleComplete: function( data ) { + var toHide = data.oldPanel, + prev = toHide.prev(); + + this._removeClass( toHide, "ui-accordion-content-active" ); + this._removeClass( prev, "ui-accordion-header-active" ) + ._addClass( prev, "ui-accordion-header-collapsed" ); + + // Work around for rendering bug in IE (#5421) + if ( toHide.length ) { + toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; + } + this._trigger( "activate", null, data ); + } +} ); + + + +var safeActiveElement = $.ui.safeActiveElement = function( document ) { + var activeElement; + + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an +
    + {% else %} {{ signed_form.agreement_text|linebreaks }} + {% endif %}
    @@ -44,7 +52,7 @@

    Actions

    - Download + Download
    @@ -52,7 +60,7 @@

    Actions

    From 9770cfa3397c0456dd06876955d875cfccdf1c94 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 6 Jun 2022 11:16:03 -0600 Subject: [PATCH 530/613] HYP-278 - Added permissions check to download API --- app/manage/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/manage/api.py b/app/manage/api.py index 7eadee7f..ad18a491 100644 --- a/app/manage/api.py +++ b/app/manage/api.py @@ -891,6 +891,15 @@ def download_submissions_export(request, project_key, fileservice_uuid): """ if request.method == "GET": + # Check permissions in SciAuthZ. + user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug("[download_submissions_export] - No Access for user " + request.user.email) + return HttpResponse("You do not have access to download this file.", status=403) + # Get filename filename = f"{project_key}_export_{fileservice_uuid}.zip" From 8a0e69254822134336d82005a895675e6def1157 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 8 Jun 2022 17:08:31 -0600 Subject: [PATCH 531/613] HYP-278 - Refactored export building procedure; fixed double-download bug --- app/manage/tasks.py | 196 +++++++++++++++++-------- app/templates/manage/project-base.html | 4 +- 2 files changed, 136 insertions(+), 64 deletions(-) diff --git a/app/manage/tasks.py b/app/manage/tasks.py index dd08e518..50fbb8dd 100644 --- a/app/manage/tasks.py +++ b/app/manage/tasks.py @@ -1,14 +1,17 @@ import uuid import os import shutil -import zipfile +import json import requests +import tempfile +from datetime import datetime from django_q.tasks import Chain, async_task from django.conf import settings from django.contrib.auth.models import User from projects.models import DataProject from projects.models import ChallengeTaskSubmission +from projects.models import ChallengeTaskSubmissionDownload from manage.models import ChallengeTaskSubmissionExport from dbmi_client import fileservice from manage.api import zip_submission_file @@ -33,67 +36,142 @@ def export_task_submissions(project_id, requester): # Get project and challenge task project = DataProject.objects.get(id=project_id) - # A list of file paths to each submission's zip file. - zipped_submissions_paths = [] - # Get all submissions made by this team for this project. submissions = ChallengeTaskSubmission.objects.filter( challenge_task__in=project.challengetask_set.all(), deleted=False ) - # For each submission, create a zip file and add the path to the list of zip files. - for submission in submissions: - try: - zip_file_path = zip_submission_file(submission, requester) - zipped_submissions_paths.append(zip_file_path) - except Exception as e: - logger.exception(f"{project.project_key}: Could not export submission '{submission.uuid}': {e}", exc_info=True) - - # Create a directory to store the final encompassing zip file. - final_zip_file_directory = "/tmp/" + str(uuid.uuid4()) - if not os.path.exists(final_zip_file_directory): - os.makedirs(final_zip_file_directory) - - # Combine all the zipped tasks into one file zip file. - final_zip_file_name = project.project_key + "__submissions.zip" - final_zip_file_path = os.path.join(final_zip_file_directory, final_zip_file_name) - with zipfile.ZipFile(final_zip_file_path, mode="w") as zf: - for zip_file in zipped_submissions_paths: - zf.write(zip_file, arcname=os.path.basename(zip_file)) - - # Perform the request to upload the file + # Create a temporary directory export_uuid = export_location = None - with open(final_zip_file_path, "rb") as file: - - # Build upload request - files = {"file": file} - try: - # Create the file in Fileservice - metadata = { - "project": project.project_key, - "type": "export", - } - tags = ["hypatio", "export", "submissions", project.project_key, requester] - export_uuid, upload_data = fileservice.create_archivefile_upload(final_zip_file_name, metadata, tags) - - # Get the location - export_location = upload_data["locationid"] - - # Upload to S3 - response = requests.post(upload_data["post"]["url"], data=upload_data["post"]["fields"], files=files) - response.raise_for_status() - - # Mark the upload as complete - fileservice.uploaded_archivefile(export_uuid, export_location) - - except KeyError as e: - logger.error('Failed export post generation: {}'.format(upload_data)) - logger.exception(e) - - except requests.exceptions.HTTPError as e: - logger.error('Failed export upload: {}'.format(upload_data)) - logger.exception(e) + with tempfile.TemporaryDirectory() as directory: + + # Create directory to put submissions in + submissions_directory_path = os.path.join(directory, f"{project.project_key}_submissions_{datetime.now().isoformat()}") + + # For each submission, create a directory for the file and its metadata file + submission_file_response = None + for submission in submissions: + try: + # Set the name of the containing directory + submission_directory_path = os.path.join(submissions_directory_path, f"{submission.participant.user.email}_{submission.uuid}") + + # Create a record of the user downloading the file. + ChallengeTaskSubmissionDownload.objects.create( + user=User.objects.get(email=requester), + submission=submission + ) + + # Create a temporary directory to hold the files specific to this submission that need to be zipped together. + if not os.path.exists(submission_directory_path): + os.makedirs(submission_directory_path) + + # Create a json file with the submission info string. + info_file_name = "submission_info.json" + with open(os.path.join(submission_directory_path, info_file_name), mode="w") as f: + f.write(submission.submission_info) + + # Determine filename + try: + submission_file_name = json.loads(submission.submission_info).get("filename") + if not submission_file_name: + + # Check fileservice + submission_file_name = fileservice.get_archivefile(submission.uuid)["filename"] + except Exception as e: + logger.exception( + f"Could not determine filename for submission", + exc_info=True, + extra={ + "submission": submission, + "archivefile_uuid": submission.uuid, + "submission_info": submission.submission_info, + } + ) + + # Use a default filename + submission_file_name = "submission_file.zip" + + # Get the submission file's byte contents from S3. + submission_file_download_url = fileservice.get_archivefile_proxy_url(uuid=submission.uuid) + headers = {"Authorization": f"{settings.FILESERVICE_AUTH_HEADER_PREFIX} {settings.FILESERVICE_SERVICE_TOKEN}"} + with requests.get(submission_file_download_url, headers=headers, stream=True) as submission_file_response: + submission_file_response.raise_for_status() + + # Write the submission file's bytes to a zip file. + with open(os.path.join(submission_directory_path, submission_file_name), mode="wb") as f: + shutil.copyfileobj(submission_file_response.raw, f) + + except requests.exceptions.HTTPError as e: + logger.exception( + f"{project.project_key}: Could not download submission '{submission.uuid}': {e}", + extra={ + "submission": submission, + "archivefile_uuid": submission.uuid, + "response": submission_file_response.content, + "status_code": submission_file_response.status_code + }) + + except Exception as e: + logger.exception( + f"{project.project_key}: Could not export submission '{submission.uuid}': {e}", + exc_info=True + ) + + # Set the archive name + archive_basename = f"{project.project_key}_submissions" + + # Archive the directory + archive_path = shutil.make_archive(archive_basename, "zip", submissions_directory_path) + + # Perform the request to upload the file + with open(archive_path, "rb") as file: + + # Build upload request + response = None + files = {"file": file} + try: + # Create the file in Fileservice + metadata = { + "project": project.project_key, + "type": "export", + } + tags = ["hypatio", "export", "submissions", project.project_key, requester] + export_uuid, upload_data = fileservice.create_archivefile_upload(os.path.basename(archive_path), metadata, tags) + + # Get the location + export_location = upload_data["locationid"] + + # Upload to S3 + response = requests.post(upload_data["post"]["url"], data=upload_data["post"]["fields"], files=files) + response.raise_for_status() + + # Mark the upload as complete + fileservice.uploaded_archivefile(export_uuid, export_location) + + except KeyError as e: + logger.error( + f'{project.project_key}: Failed export post generation: {upload_data}', + exc_info=True + ) + raise e + + except requests.exceptions.HTTPError as e: + logger.exception( + f'{project.project_key}: Failed export upload: {upload_data}', + extra={ + "response": response.content, + "status_code": response.status_code + } + ) + raise e + + except Exception as e: + logger.exception( + f"{project.project_key}: Could not export submissions: {e}", + exc_info=True, + ) + raise e # Create the model entry for the export export = ChallengeTaskSubmissionExport.objects.create( @@ -116,13 +194,6 @@ def export_task_submissions(project_id, requester): extra={"site_url": settings.SITE_URL, "project": project} ) - # Delete all the directories holding the zip files. - for path in zipped_submissions_paths: - shutil.rmtree(os.path.dirname(os.path.realpath((path)))) - - # Delete the final zip file. - shutil.rmtree(final_zip_file_directory) - except Exception as e: logger.exception( f"Export challenge task submissions error: {e}", @@ -132,3 +203,4 @@ def export_task_submissions(project_id, requester): "requester": requester, } ) + raise e diff --git a/app/templates/manage/project-base.html b/app/templates/manage/project-base.html index 3ef5c8af..581548cf 100644 --- a/app/templates/manage/project-base.html +++ b/app/templates/manage/project-base.html @@ -459,9 +459,9 @@

    Submissions Exports

    {% endfor %} From d532abfee64535c49ee7babf3cd084bd04b86c4e Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 11 Jun 2022 15:18:49 -0600 Subject: [PATCH 532/613] HYP-HOTFIX-061122: Updated base image for security updates --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e28cdb00..c596f2c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.3.3 AS builder +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 AS builder # Install requirements RUN apt-get update \ @@ -19,7 +19,7 @@ RUN pip install -U wheel \ && pip wheel -r /requirements.txt \ --wheel-dir=/root/wheels -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.3.3 +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels From c6f0f5b66be7d0107945f969517b5c7077a80809 Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 13 Jun 2022 12:08:27 +0000 Subject: [PATCH 533/613] fix(requirements): Updated Python requirements --- requirements.txt | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/requirements.txt b/requirements.txt index ddb5bc8b..902079f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,21 +8,21 @@ arrow==1.2.2 \ --hash=sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b \ --hash=sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177 # via django-q -awscli==1.25.2 \ - --hash=sha256:8433696101572b2d7dff9c8b60f3d6355308d9b758fe4765747a4cff12eeef88 \ - --hash=sha256:b3309b5e76f8c95904a071088ef6e8a3dee3f03ddd941807176f1cdf044b7496 +awscli==1.25.7 \ + --hash=sha256:0ed4ef063e7417a8ae80f4186614d3c28f442e3ec04baa2233fb77ee310ceaea \ + --hash=sha256:b141a2cdd07003e267e5740b84f239f38375be7d836b2aacbc92db346d6c8764 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.2 \ - --hash=sha256:927b5e8e2decad746e6c32bb81f15c2ea9ab4398286134d21f6742493eb893f6 \ - --hash=sha256:e3c10adc7be890b147568a4162d9cafb876f11f87460c4a0dc90742d6d4ebe7c +boto3==1.24.7 \ + --hash=sha256:6e243e28c804dccd2015935acfac0567e1861b20fdd96aa47f232b47aa214a69 \ + --hash=sha256:925a34a55257219f4601e803951fd4d61ed6eac2208dc834a04fe150b03f265e # via -r requirements.in -botocore==1.27.2 \ - --hash=sha256:131f71fe16ef84f9e0e72c54d2e230a6d8e79dd3947f507259a129649649a35d \ - --hash=sha256:b7cdd4f4a6395a084a381a7d2a25b177e6de5f8a4dfa3c645ec957ba3c83e200 +botocore==1.27.7 \ + --hash=sha256:3e0cbe26f08fe9a3f6df5de4dcc3bef686e01ba5f79ad03ffbe79d92f51ecea5 \ + --hash=sha256:dc83ef991c730ab0f06b51fcefda74f493b990903b882452aff78c123e3040e2 # via # awscli # boto3 @@ -249,9 +249,9 @@ python-dateutil==2.8.2 \ # -r requirements.in # arrow # botocore -python-magic==0.4.26 \ - --hash=sha256:8262c13001f904ad5b724d38b5e5b5f17ec0450ae249def398a62e4e33108a50 \ - --hash=sha256:b978c4b69a20510d133a7f488910c2f07e7796f1f31703e61c241973f2bbf5fb +python-magic==0.4.27 \ + --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ + --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in pytz==2022.1 \ --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ @@ -300,9 +300,9 @@ redis==3.5.3 \ --hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 \ --hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 # via django-q -requests==2.27.1 \ - --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ - --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d +requests==2.28.0 \ + --hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \ + --hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b # via # -r requirements.in # django-dbmi-client From 348308886a007c120a38ff1e0efd3af3730082f5 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 30 Jun 2022 11:48:03 -0600 Subject: [PATCH 534/613] HYP-283 - Added a management command for exporting signed agreement forms --- .../commands/signed_agreement_form_export.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 app/projects/management/commands/signed_agreement_form_export.py diff --git a/app/projects/management/commands/signed_agreement_form_export.py b/app/projects/management/commands/signed_agreement_form_export.py new file mode 100644 index 00000000..f4d3754d --- /dev/null +++ b/app/projects/management/commands/signed_agreement_form_export.py @@ -0,0 +1,139 @@ +import sys +import tempfile +import requests +import shutil +import boto3 +import os +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from dbmi_client import fileservice + +from projects.models import DataProject +from projects.models import SignedAgreementForm +from projects.models import AgreementForm + +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Export signed agreement forms' + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('project_key', type=str) + parser.add_argument('agreement_form', type=str) + + def handle(self, *args, **options): + + # Ensure it exists + if not DataProject.objects.filter(project_key=options['project_key']).exists(): + raise CommandError(f'Project with key "{options["project_key"]}" does not exist') + if not AgreementForm.objects.filter(short_name=options['agreement_form']).exists(): + raise CommandError(f'Agreement Form with name "{options["agreement_form"]}" does not exist') + + # Get the objects + project = DataProject.objects.get(project_key=options['project_key']) + agreement_form = AgreementForm.objects.get(short_name=options['agreement_form']) + signed_agreement_forms = SignedAgreementForm.objects.filter(project=project, agreement_form=agreement_form) + + # Ensure we've got Qualtrics surveys + if not signed_agreement_forms: + self.stdout.write( + f'{project.project_key}/{agreement_form.name}: Does not have any signed agreement forms' + ) + return + + export_uuid = export_location = export_url = None + try: + # Create a temporary directory + with tempfile.TemporaryDirectory() as directory: + + # Set the archive name + archive_root = os.path.join(directory, "signed_agreement_forms") + os.makedirs(archive_root) + archive_basename = f"{project.project_key}_{agreement_form.short_name}" + + # Create boto client + s3 = boto3.client("s3") + + # Download DUAs + for signed_agreement_form in signed_agreement_forms: + + try: + # Set the key + key = os.path.join(settings.AWS_LOCATION, signed_agreement_form.upload.name) + + # Download the file + s3.download_file(settings.AWS_STORAGE_BUCKET_NAME, key, os.path.join(archive_root, signed_agreement_form.upload.name)) + + except Exception as e: + self.stdout.write(self.style.ERROR( + f"Error: Could not download signed agreement form file: {e}" + )) + + # Archive the directory + archive_path = shutil.make_archive(archive_basename, "zip", archive_root) + logger.debug(f"Export archive: {archive_path}") + + # Perform the request to upload the file + with open(archive_path, "rb") as file: + + # Build upload request + response = None + files = {"file": file} + try: + # Create the file in Fileservice + metadata = { + "project": project.project_key, + "agreement_form": agreement_form.short_name, + "type": "export", + } + tags = ["hypatio", "export", "signed-agreement-forms", project.project_key, ] + export_uuid, upload_data = fileservice.create_archivefile_upload(os.path.basename(archive_path), metadata, tags) + + # Get the location + export_location = upload_data["locationid"] + + # Upload to S3 + response = requests.post(upload_data["post"]["url"], data=upload_data["post"]["fields"], files=files) + response.raise_for_status() + + # Mark the upload as complete + fileservice.uploaded_archivefile(export_uuid, export_location) + + # Get the download URL + export_url = fileservice.get_archivefile_download_url(export_uuid) + + except KeyError as e: + logger.error( + f'{project.project_key}: Failed export post generation: {upload_data}', + exc_info=True + ) + raise e + + except requests.exceptions.HTTPError as e: + logger.exception( + f'{project.project_key}: Failed export upload: {upload_data}', + extra={ + "response": response.content, + "status_code": response.status_code + } + ) + raise e + + except Exception as e: + logger.exception( + f"{project.project_key}: Could not export submissions: {e}", + exc_info=True, + ) + raise e + + # Return export UUID + self.stdout.write(f"Export: {export_url}") + + except Exception as e: + self.stdout.write(self.style.ERROR( + f"Error: {e}" + )) + sys.exit(1) From a23b095fee85a742ee9e9464cda75e3e3ae8ceef Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 30 Jun 2022 12:24:35 -0600 Subject: [PATCH 535/613] HYP-283 - Set status filter on signed agreement forms --- .../management/commands/signed_agreement_form_export.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/projects/management/commands/signed_agreement_form_export.py b/app/projects/management/commands/signed_agreement_form_export.py index f4d3754d..40d4e046 100644 --- a/app/projects/management/commands/signed_agreement_form_export.py +++ b/app/projects/management/commands/signed_agreement_form_export.py @@ -23,6 +23,7 @@ def add_arguments(self, parser): # Positional arguments parser.add_argument('project_key', type=str) parser.add_argument('agreement_form', type=str) + parser.add_argument('status', type=str, default='A') def handle(self, *args, **options): @@ -35,7 +36,11 @@ def handle(self, *args, **options): # Get the objects project = DataProject.objects.get(project_key=options['project_key']) agreement_form = AgreementForm.objects.get(short_name=options['agreement_form']) - signed_agreement_forms = SignedAgreementForm.objects.filter(project=project, agreement_form=agreement_form) + signed_agreement_forms = SignedAgreementForm.objects.filter( + project=project, + agreement_form=agreement_form, + status=options["status"], + ) # Ensure we've got Qualtrics surveys if not signed_agreement_forms: From c2baff034fe213b8dc85d8819d2b2ebe0420f867 Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:06:14 -0400 Subject: [PATCH 536/613] Update n2c2-submissions.html Track 3 --- .../submissionforms/n2c2-submissions.html | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/static/submissionforms/n2c2-submissions.html b/app/static/submissionforms/n2c2-submissions.html index 472bc628..9b8e1807 100644 --- a/app/static/submissionforms/n2c2-submissions.html +++ b/app/static/submissionforms/n2c2-submissions.html @@ -42,6 +42,43 @@ +
    + +
    + + +
    +
    + +
    + +
    + + + +
    +
    + +
    + + +
    + +
    + + +
    +
    From af4058dee83c914bd663a3d3aa75cdc49ef027db Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:39:10 -0400 Subject: [PATCH 537/613] Update n2c2-submissions.html number questions; radio buttons inline q7 --- .../submissionforms/n2c2-submissions.html | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/static/submissionforms/n2c2-submissions.html b/app/static/submissionforms/n2c2-submissions.html index 9b8e1807..6fe18dda 100644 --- a/app/static/submissionforms/n2c2-submissions.html +++ b/app/static/submissionforms/n2c2-submissions.html @@ -1,17 +1,17 @@
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    -
    - +
    - +
    - +
    \ No newline at end of file From b2e552d1b33b37bb05a60cdcc948f1edc0a91394 Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Thu, 14 Jul 2022 20:11:24 -0400 Subject: [PATCH 538/613] Update n2c2-submissions.html --- app/static/submissionforms/n2c2-submissions.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/submissionforms/n2c2-submissions.html b/app/static/submissionforms/n2c2-submissions.html index 6fe18dda..d81dfc7b 100644 --- a/app/static/submissionforms/n2c2-submissions.html +++ b/app/static/submissionforms/n2c2-submissions.html @@ -71,12 +71,12 @@
    - +
    - +
    From af02450ff1df9a87787438d621166d80a9c39b73 Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Thu, 14 Jul 2022 20:27:23 -0400 Subject: [PATCH 539/613] Update n2c2-submissions.html --- app/static/submissionforms/n2c2-submissions.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/submissionforms/n2c2-submissions.html b/app/static/submissionforms/n2c2-submissions.html index d81dfc7b..19016443 100644 --- a/app/static/submissionforms/n2c2-submissions.html +++ b/app/static/submissionforms/n2c2-submissions.html @@ -71,7 +71,7 @@
    - +
    From 9b6636e23a840fdce86f5dd13ed0b69603c369e1 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 25 Jul 2022 18:17:31 -0600 Subject: [PATCH 540/613] DBMISVC-HOTFIX-072522 - Pinned django-picklefield and updated requirements --- requirements.in | 1 + requirements.txt | 228 +++++++++++++++++++++++++---------------------- 2 files changed, 124 insertions(+), 105 deletions(-) diff --git a/requirements.in b/requirements.in index 866c1da8..fb8f39e8 100644 --- a/requirements.in +++ b/requirements.in @@ -9,6 +9,7 @@ django-dbmi-client<2.0 django-health-check<4.0 django-jquery<4.0 django-jsonfield-backport<2.0 +django-picklefield<3.1 django-storages<2.0 django-stronghold<=1.0 djangorestframework<4.0 diff --git a/requirements.txt b/requirements.txt index 902079f5..94b14692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,112 +8,126 @@ arrow==1.2.2 \ --hash=sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b \ --hash=sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177 # via django-q -awscli==1.25.7 \ - --hash=sha256:0ed4ef063e7417a8ae80f4186614d3c28f442e3ec04baa2233fb77ee310ceaea \ - --hash=sha256:b141a2cdd07003e267e5740b84f239f38375be7d836b2aacbc92db346d6c8764 +awscli==1.25.37 \ + --hash=sha256:43e6062245ffcbc3b2903c7c5255ab573eea29f5f14be9c1b20e433f7b338a51 \ + --hash=sha256:878b985f1587fa69bbb2193c138c9851b5f0369c9a140b1a33e6a519d1a0a876 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.7 \ - --hash=sha256:6e243e28c804dccd2015935acfac0567e1861b20fdd96aa47f232b47aa214a69 \ - --hash=sha256:925a34a55257219f4601e803951fd4d61ed6eac2208dc834a04fe150b03f265e +boto3==1.24.37 \ + --hash=sha256:801c64aa3dbeabbf2e06e27bc3530245ebc6b14b6d7e8bcff08ff67ec6e60c88 \ + --hash=sha256:c5f0b44a77d01d6f714f91f181b1e0819fed9b99423234cd4c2cce6704e915b3 # via -r requirements.in -botocore==1.27.7 \ - --hash=sha256:3e0cbe26f08fe9a3f6df5de4dcc3bef686e01ba5f79ad03ffbe79d92f51ecea5 \ - --hash=sha256:dc83ef991c730ab0f06b51fcefda74f493b990903b882452aff78c123e3040e2 +botocore==1.27.37 \ + --hash=sha256:4616a7bb869b890c4b582460423e04447dd24b4af76ac761ec68236b6cb0ef7a \ + --hash=sha256:c73988eff91897fe840ae6a9b459705e3086bc7f62f37a12999a7f4564002f63 # via # awscli # boto3 # s3transfer -certifi==2022.5.18.1 \ - --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \ - --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a +certifi==2022.6.15 \ + --hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \ + --hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412 # via requests -cffi==1.15.0 \ - --hash=sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3 \ - --hash=sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2 \ - --hash=sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636 \ - --hash=sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20 \ - --hash=sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728 \ - --hash=sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27 \ - --hash=sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66 \ - --hash=sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443 \ - --hash=sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0 \ - --hash=sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7 \ - --hash=sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39 \ - --hash=sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605 \ - --hash=sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a \ - --hash=sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37 \ - --hash=sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029 \ - --hash=sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139 \ - --hash=sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc \ - --hash=sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df \ - --hash=sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14 \ - --hash=sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880 \ - --hash=sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2 \ - --hash=sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a \ - --hash=sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e \ - --hash=sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474 \ - --hash=sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024 \ - --hash=sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8 \ - --hash=sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0 \ - --hash=sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e \ - --hash=sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a \ - --hash=sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e \ - --hash=sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032 \ - --hash=sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6 \ - --hash=sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e \ - --hash=sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b \ - --hash=sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e \ - --hash=sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954 \ - --hash=sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962 \ - --hash=sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c \ - --hash=sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4 \ - --hash=sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55 \ - --hash=sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962 \ - --hash=sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023 \ - --hash=sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c \ - --hash=sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6 \ - --hash=sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8 \ - --hash=sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382 \ - --hash=sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7 \ - --hash=sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc \ - --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ - --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 +cffi==1.15.1 \ + --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ + --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ + --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ + --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ + --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ + --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ + --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ + --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ + --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ + --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ + --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ + --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ + --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ + --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ + --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ + --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ + --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ + --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ + --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ + --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ + --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ + --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ + --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ + --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ + --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ + --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ + --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ + --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ + --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ + --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ + --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ + --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ + --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ + --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ + --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ + --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ + --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ + --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ + --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ + --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ + --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ + --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ + --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ + --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ + --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ + --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ + --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ + --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ + --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ + --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ + --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ + --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ + --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ + --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ + --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ + --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ + --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ + --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ + --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ + --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ + --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ + --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ + --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ + --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 # via cryptography -charset-normalizer==2.0.12 \ - --hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \ - --hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df +charset-normalizer==2.1.0 \ + --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \ + --hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 # via requests colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==37.0.2 \ - --hash=sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804 \ - --hash=sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178 \ - --hash=sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717 \ - --hash=sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982 \ - --hash=sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004 \ - --hash=sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe \ - --hash=sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452 \ - --hash=sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336 \ - --hash=sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4 \ - --hash=sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15 \ - --hash=sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d \ - --hash=sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c \ - --hash=sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0 \ - --hash=sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06 \ - --hash=sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9 \ - --hash=sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1 \ - --hash=sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023 \ - --hash=sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de \ - --hash=sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f \ - --hash=sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181 \ - --hash=sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e \ - --hash=sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a +cryptography==37.0.4 \ + --hash=sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59 \ + --hash=sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596 \ + --hash=sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3 \ + --hash=sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5 \ + --hash=sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab \ + --hash=sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884 \ + --hash=sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82 \ + --hash=sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b \ + --hash=sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441 \ + --hash=sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa \ + --hash=sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d \ + --hash=sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b \ + --hash=sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a \ + --hash=sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6 \ + --hash=sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157 \ + --hash=sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280 \ + --hash=sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282 \ + --hash=sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67 \ + --hash=sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8 \ + --hash=sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046 \ + --hash=sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327 \ + --hash=sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9 # via # django-dbmi-client # jwcrypto @@ -171,7 +185,9 @@ django-jsonfield-backport==1.0.5 \ django-picklefield==3.0.1 \ --hash=sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287 \ --hash=sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35 - # via django-q + # via + # -r requirements.in + # django-q django-q==1.3.9 \ --hash=sha256:1b74ce3a8931990b136903e3a7bc9b07243282a2b5355117246f05ed5d076e68 \ --hash=sha256:5c6b4d530aa3aabf9c6aa57376da1ca2abf89a1562b77038b7a04e52a4a0a91b @@ -206,9 +222,9 @@ idna==3.3 \ --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d # via requests -jmespath==1.0.0 \ - --hash=sha256:a490e280edd1f57d6de88636992d05b71e97d69a26a19f058ecf7d304474bf5e \ - --hash=sha256:e8dcd576ed616f14ec02eed0005c85973b5890083313860136657e24784e4c04 +jmespath==1.0.1 \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe # via # boto3 # botocore @@ -217,12 +233,14 @@ jwcrypto==1.3.1 \ # via # -r requirements.in # django-dbmi-client -mysqlclient==2.1.0 \ - --hash=sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947 \ - --hash=sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c \ - --hash=sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12 \ - --hash=sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b \ - --hash=sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14 +mysqlclient==2.1.1 \ + --hash=sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c \ + --hash=sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782 \ + --hash=sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855 \ + --hash=sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994 \ + --hash=sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37 \ + --hash=sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b \ + --hash=sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96 # via -r requirements.in orderedmultidict==1.0.1 \ --hash=sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad \ @@ -300,9 +318,9 @@ redis==3.5.3 \ --hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 \ --hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 # via django-q -requests==2.28.0 \ - --hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \ - --hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b +requests==2.28.1 \ + --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ + --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 # via # -r requirements.in # django-dbmi-client @@ -329,9 +347,9 @@ sqlparse==0.4.2 \ --hash=sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae \ --hash=sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d # via django -urllib3==1.26.9 \ - --hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \ - --hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e +urllib3==1.26.11 \ + --hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \ + --hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a # via # botocore # requests From f79165b49ba6eef19dec01b5cbf285bb9139cde0 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 26 Jul 2022 10:01:21 -0600 Subject: [PATCH 541/613] HYP-285 - Added two admin commands to manage challenge task operations --- .../commands/list_no_submissions.py | 147 ++++++++++++++++++ .../commands/revoke_access_no_submissions.py | 132 ++++++++++++++++ .../email/email_operation_report.html | 24 +++ .../email/email_operation_report.txt | 9 ++ 4 files changed, 312 insertions(+) create mode 100644 app/projects/management/commands/list_no_submissions.py create mode 100644 app/projects/management/commands/revoke_access_no_submissions.py create mode 100644 app/templates/email/email_operation_report.html create mode 100644 app/templates/email/email_operation_report.txt diff --git a/app/projects/management/commands/list_no_submissions.py b/app/projects/management/commands/list_no_submissions.py new file mode 100644 index 00000000..fda57296 --- /dev/null +++ b/app/projects/management/commands/list_no_submissions.py @@ -0,0 +1,147 @@ +import json +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from dbmi_client import fileservice + +from projects.models import DataProject +from projects.models import Team, TEAM_ACTIVE, TEAM_DEACTIVATED +from projects.models import Participant +from projects.models import HostedFile +from projects.models import HostedFileDownload + +from contact.views import email_send + +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'List teams/participants that downloaded files for a project but did not provide submissions' + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('project_key', type=str) + + # Optional arguments + parser.add_argument('-r', '--recipient', type=str, help='The recipient for operation report', ) + + def email_report(self, project_key, recipient, report, *args, **options): + """ + Sends a report of the results of the operation. + """ + # Set context + context={ + "operation": self.help, + "project": project_key, + "message": report, + } + + # Send it out. + success = email_send( + subject=f'DBMI Portal - Operation Report', + recipients=[recipient], + email_template='email_operation_report', + extra=context + ) + if success: + self.stdout.write(self.style.SUCCESS(f"Report sent to: {recipient}")) + else: + self.stdout.write(self.style.ERROR(f"Report failed to send to: {recipient}")) + + def handle(self, *args, **options): + + # Ensure it exists + if not DataProject.objects.filter(project_key=options['project_key']).exists(): + raise CommandError(f'Project with key "{options["project_key"]}" does not exist') + + # Get the objects + project = DataProject.objects.get(project_key=options['project_key']) + + # Determine if a team-based challenge or not + if project.has_teams: + + # Fetch teams + teams = Team.objects.filter(data_project=project) + + # Filter out teams without submissions + teams_without_submissions = [t for t in teams if not t.get_submissions()] + + # Get all hosted files for the project + hosted_files = HostedFile.objects.filter(project=project) + + # Track teams with no submissions but did download hosted files + teams_with_access_and_no_submissions = [] + + # Find teams with a user that downloaded any of the files for the project + for team in teams_without_submissions: + + # Iterate participants + for participant in team.participant_set.all(): + + # Find downloads + if HostedFileDownload.objects.filter(hosted_file__in=hosted_files, user=participant.user): + + # Add them + teams_with_access_and_no_submissions.append(team) + break + + # Build report object + report = { + "total_teams_with_downloads_and_no_submissions": len(teams_with_access_and_no_submissions), + "teams_with_downloads_and_no_submissions": [ + {"team_leader": team.team_leader.email, "team_id": team.id} + for team in teams_with_access_and_no_submissions + ], + "total_active_teams": len(teams), + } + + # Output + self.stdout.write(self.style.SUCCESS(json.dumps(report))) + + # Check for recipient + if options['recipient']: + + # Send it + self.email_report(options['project_key'], options['recipient'], json.dumps(report)) + + else: + + # Get all participants + participants = Participant.objects.filter(project=project) + + # Filter out teams without submissions + participants_without_submissions = [p for p in participants if not p.get_submissions()] + + # Get all hosted files for the project + hosted_files = HostedFile.objects.filter(project=project) + + # Track teams with no submissions but did download hosted files + participants_with_access_and_no_submissions = [] + + # Find teams with a user that downloaded any of the files for the project + for participant in participants_without_submissions: + + # Find downloads + if HostedFileDownload.objects.filter(hosted_file__in=hosted_files, user=participant.user): + + # Add them + participants_with_access_and_no_submissions.append(participant) + + # Build report object + report = { + "total_participants_with_downloads_and_no_submissions": len(participants_with_access_and_no_submissions), + "participants_with_downloads_and_no_submissions": [ + {"email": participant.user.email, "participant_id": participant.id} + for participant in participants_with_access_and_no_submissions + ], + "total_active_participants": len(participants), + } + + # Output + self.stdout.write(self.style.SUCCESS(json.dumps(report))) + + # Check for recipient + if options['recipient']: + + # Send it + self.email_report(options['project_key'], options['recipient'], json.dumps(report)) diff --git a/app/projects/management/commands/revoke_access_no_submissions.py b/app/projects/management/commands/revoke_access_no_submissions.py new file mode 100644 index 00000000..6a3ee173 --- /dev/null +++ b/app/projects/management/commands/revoke_access_no_submissions.py @@ -0,0 +1,132 @@ +import json + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from projects.models import DataProject +from projects.models import Team, TEAM_ACTIVE, TEAM_DEACTIVATED +from projects.models import Participant +from contact.views import email_send + +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Revoke access for individuals/teams without submissions' + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('project_key', type=str) + + # Optional arguments + parser.add_argument('-r', '--recipient', type=str, help='The recipient for operation report', ) + parser.add_argument('-c', '--commit', action='store_true', help='Commit revocations for participants/teams', ) + + def email_report(self, project_key, recipient, report, *args, **options): + """ + Sends a report of the results of the operation. + """ + # Set context + context={ + "operation": self.help, + "project": project_key, + "message": report, + } + + # Send it out. + success = email_send( + subject=f'DBMI Portal - Operation Report', + recipients=[recipient], + email_template='email_operation_report', + extra=context + ) + if success: + self.stdout.write(self.style.SUCCESS(f"Report sent to: {recipient}")) + else: + self.stdout.write(self.style.ERROR(f"Report failed to send to: {recipient}")) + + + def handle(self, *args, **options): + + # Ensure it exists + if not DataProject.objects.filter(project_key=options['project_key']).exists(): + raise CommandError(f'Project with key "{options["project_key"]}" does not exist') + + # Get the objects + project = DataProject.objects.get(project_key=options['project_key']) + + # Determine if a team-based challenge or not + if project.has_teams: + + # Fetch teams + teams = Team.objects.filter(data_project=project, status=TEAM_ACTIVE) + + # Filter out teams without submissions + teams_without_submissions = [t for t in teams if not t.get_submissions()] + + # Iterate the list + for team in teams_without_submissions: + + # Check if only listing + if options['commit']: + + # Revoke access for the team + team.status = TEAM_DEACTIVATED + team.save() + + # Build report object + report = { + "total_revoked_teams": len(teams_without_submissions), + "revoked_teams": [ + {"team_leader": team.team_leader.email, "team_id": team.id} + for team in teams_without_submissions + ], + "total_active_teams": len(teams), + } + + # Output + self.stdout.write(self.style.SUCCESS(json.dumps(report))) + + # Check for recipient + if options['recipient']: + + # Send it + self.email_report(options['project_key'], options['recipient'], json.dumps(report)) + + else: + + # Get all participants + participants = Participant.objects.filter(project=project, permission="VIEW") + + # Filter out teams without submissions + participants_without_submissions = [p for p in participants if not p.get_submissions()] + + # Get all participants + for participant in participants_without_submissions: + + # Check if only listing + if options['commit']: + + # Revoke access for the participant + participant.permission = None + participant.save() + + # Build report object + report = { + "total_revoked_participants": len(participants_without_submissions), + "revoked_participants": [ + {"email": participant.user.email, "participant_id": participant.id} + for participant in participants_without_submissions + ], + "total_active_participants": len(participants), + } + + # Output + self.stdout.write(self.style.SUCCESS(json.dumps(report))) + + # Check for recipient + if options['recipient']: + + # Send it + self.email_report(options['project_key'], options['recipient'], json.dumps(report)) diff --git a/app/templates/email/email_operation_report.html b/app/templates/email/email_operation_report.html new file mode 100644 index 00000000..2ed3046d --- /dev/null +++ b/app/templates/email/email_operation_report.html @@ -0,0 +1,24 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Operation Report{% endblock %} + +{% block content %} + +

    + {% if project %} + {{ project|upper }} Operation + {% else %} + Hypatio Operation + {% endif %} +

    +

    + {{ operation }} +

    + +
    + +

    + {{ message }} +

    + +{% endblock %} diff --git a/app/templates/email/email_operation_report.txt b/app/templates/email/email_operation_report.txt new file mode 100644 index 00000000..4e06a0e2 --- /dev/null +++ b/app/templates/email/email_operation_report.txt @@ -0,0 +1,9 @@ +{% if project %} + {{ project|upper }} Operation +{% else %} + Hypatio Operation +{% endif %} + +{{ operation }} +---------------------------------------------------- +{{ message }} From f6eb7cb48ac0dae78c7c1bfefb21d1360204848b Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 26 Jul 2022 10:02:01 -0600 Subject: [PATCH 542/613] HYP-285 - Set as modules --- app/projects/management/__init__.py | 0 app/projects/management/commands/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/projects/management/__init__.py create mode 100644 app/projects/management/commands/__init__.py diff --git a/app/projects/management/__init__.py b/app/projects/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/projects/management/commands/__init__.py b/app/projects/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b From 6c97ead93b68e9e738df8fb863afa8a951175b66 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 26 Jul 2022 13:55:31 -0600 Subject: [PATCH 543/613] HYP-285 - Added specific task for n2c2-2022-t2 --- .../commands/n2c2-2022-t2-revoke.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/projects/management/commands/n2c2-2022-t2-revoke.py diff --git a/app/projects/management/commands/n2c2-2022-t2-revoke.py b/app/projects/management/commands/n2c2-2022-t2-revoke.py new file mode 100644 index 00000000..0912d926 --- /dev/null +++ b/app/projects/management/commands/n2c2-2022-t2-revoke.py @@ -0,0 +1,98 @@ +import json + +from django.conf import settings +from django.core.management.base import BaseCommand + +from projects.models import DataProject +from projects.models import Team, TEAM_ACTIVE, TEAM_DEACTIVATED +from contact.views import email_send + +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Revoke access for teams that have not submitted for any subtasks' + + def add_arguments(self, parser): + + # Optional arguments + parser.add_argument('-r', '--recipient', type=str, help='The recipient for operation report', ) + parser.add_argument('-c', '--commit', action='store_true', help='Commit revocations for participants/teams', ) + + def email_report(self, project_key, recipient, report, *args, **options): + """ + Sends a report of the results of the operation. + """ + # Set context + context={ + "operation": self.help, + "project": project_key, + "message": report, + } + + # Send it out. + success = email_send( + subject=f'DBMI Portal - Operation Report', + recipients=[recipient], + email_template='email_operation_report', + extra=context + ) + if success: + self.stdout.write(self.style.SUCCESS(f"Report sent to: {recipient}")) + else: + self.stdout.write(self.style.ERROR(f"Report failed to send to: {recipient}")) + + + def handle(self, *args, **options): + + # Get the objects + project = DataProject.objects.get(project_key="n2c2-2022-t2") + + # Collect teams without submissions + teams_without_submissions = [] + + # Fetch teams + teams = Team.objects.filter(data_project=project, status=TEAM_ACTIVE) + + # Iterate teams + for team in teams: + + # Get all sub teams + sub_teams = Team.objects.filter(source=team) + + # Check if any have submissions + has_submissions = next((t for t in sub_teams if t.get_submissions()), None) + + # Add to the list if nothing + if not has_submissions: + teams_without_submissions.append(team) + + # Iterate the list + for team in teams_without_submissions: + + # Check if only listing + if options['commit']: + + # Revoke access for the team + team.status = TEAM_DEACTIVATED + team.save() + + # Build report object + report = { + "total_revoked_teams": len(teams_without_submissions), + "revoked_teams": [ + {"team_leader": team.team_leader.email, "team_id": team.id} + for team in teams_without_submissions + ], + "total_active_teams": len(teams), + } + + # Output + self.stdout.write(self.style.SUCCESS(json.dumps(report))) + + # Check for recipient + if options['recipient']: + + # Send it + self.email_report("n2c2-2022-t2", options['recipient'], json.dumps(report)) From 6588b1aef36aad4e829f1651d71f96cf030b2c99 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 9 Aug 2022 12:25:37 -0600 Subject: [PATCH 544/613] HYP-288 - Added internal agreement forms for uploading and use by administrators only --- app/manage/forms.py | 18 ++- app/manage/urls.py | 2 + app/manage/views.py | 111 +++++++++++++++++- .../migrations/0093_agreementform_internal.py | 18 +++ app/projects/models.py | 1 + app/templates/manage/team.html | 12 ++ .../manage/upload-signed-agreement-form.html | 20 ++++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 app/projects/migrations/0093_agreementform_internal.py create mode 100644 app/templates/manage/upload-signed-agreement-form.html diff --git a/app/manage/forms.py b/app/manage/forms.py index f2f46ae4..4d142c45 100644 --- a/app/manage/forms.py +++ b/app/manage/forms.py @@ -3,9 +3,10 @@ from bootstrap_datepicker_plus import DateTimePickerInput from dal import autocomplete -from projects.models import DataProject +from projects.models import AgreementForm, DataProject from projects.models import HostedFile from projects.models import Team +from projects.models import AGREEMENT_FORM_TYPE_FILE # TODO Convert all other manual forms into Django forms # ... @@ -63,3 +64,18 @@ class NotificationForm(forms.Form): project = forms.ModelChoiceField(queryset=DataProject.objects.all(), widget=forms.HiddenInput) message = forms.CharField(label='Message', required=True, widget=forms.Textarea) team = forms.ModelChoiceField(queryset=Team.objects.all(), widget=forms.HiddenInput) + + +class UploadSignedAgreementFormForm(forms.Form): + agreement_form = forms.ModelChoiceField(queryset=AgreementForm.objects.filter(type=AGREEMENT_FORM_TYPE_FILE, internal=True), widget=forms.Select(attrs={'class': 'form-control'})) + project_key = forms.CharField(label='Project Key', max_length=128, required=True, widget=forms.HiddenInput()) + participant = forms.CharField(label='Participant', max_length=128, required=True, widget=forms.HiddenInput()) + signed_agreement_form = forms.FileField(label="Signed Agreement Form PDF", required=True) + + def __init__(self, *args, **kwargs): + project_key = kwargs.pop('project_key', None) + super(UploadSignedAgreementFormForm, self).__init__(*args, **kwargs) + + # Limit agreement form choices to those related to the passed project + if project_key: + self.fields['agreement_form'].queryset = DataProject.objects.get(project_key=project_key).agreement_forms.all() diff --git a/app/manage/urls.py b/app/manage/urls.py index 4cbec266..fae3d753 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -6,6 +6,7 @@ from manage.views import manage_team from manage.views import ProjectParticipants from manage.views import team_notification +from manage.views import UploadSignedAgreementFormView from manage.api import set_dataproject_details from manage.api import set_dataproject_registration_status @@ -58,6 +59,7 @@ url(r'^grant-view-permission/(?P[^/]+)/(?P[^/]+)/$', grant_view_permission, name='grant-view-permission'), url(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), url(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), + url(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), url(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), url(r'^(?P[^/]+)/(?P[^/]+)/$', manage_team, name='manage-team'), ] diff --git a/app/manage/views.py b/app/manage/views.py index b292f85c..f196b56c 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -1,5 +1,5 @@ import logging - +from datetime import datetime from hypatio.auth0authenticate import user_auth_and_jwt from django.conf import settings @@ -25,6 +25,7 @@ from manage.forms import NotificationForm from manage.models import ChallengeTaskSubmissionExport +from manage.forms import UploadSignedAgreementFormForm from projects.models import AgreementForm, ChallengeTaskSubmission from projects.models import DataProject from projects.models import Participant @@ -33,6 +34,7 @@ from projects.models import SignedAgreementForm from projects.models import HostedFile from projects.models import HostedFileDownload +from projects.models import SIGNED_FORM_APPROVED # Get an instance of a logger logger = logging.getLogger(__name__) @@ -604,6 +606,15 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht team_accepted_forms += 1 signed_accepted_agreement_forms += 1 + # Add internal signed agreement forms + for signed_agreement_form in SignedAgreementForm.objects.filter( + agreement_form__internal=True, + user__email=email, + project=project): + + # Add it + signed_agreement_forms.append(signed_agreement_form) + team_member_details.append({ 'email': email, 'user_info': user_info, @@ -643,3 +654,101 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht } return render(request, template_name, context=context) + + +@method_decorator([user_auth_and_jwt], name='dispatch') +class UploadSignedAgreementFormView(View): + """ + View to upload signed agreement forms for participants. + + * Requires token authentication. + * Only admin users are able to access this view. + """ + def get(self, request, project_key, user_email, *args, **kwargs): + """ + Return the upload form template + """ + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format( + email=user.email, + project_key=project_key + )) + return HttpResponse(403) + + # Return file upload form + form = UploadSignedAgreementFormForm(initial={ + "project_key": project_key, + "participant": user_email, + }) + + # Set context + context = { + "form": form, + "project_key": project_key, + "user_email": user_email, + } + + # Render html + return render(request, "manage/upload-signed-agreement-form.html", context) + + def post(self, request, project_key, user_email, *args, **kwargs): + """ + Process the form + """ + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format( + email=user.email, + project_key=project_key + )) + return HttpResponse(403) + + # Assembles the form and run validation. + form = UploadSignedAgreementFormForm(data=request.POST, files=request.FILES) + if not form.is_valid(): + logger.warning('Form failed: {}'.format(form.errors.as_json())) + return HttpResponse(status=400) + + logger.debug(f"[upload_signed_agreement_form] Data -> {form.cleaned_data}") + + signed_agreement_form = form.cleaned_data['signed_agreement_form'] + agreement_form = form.cleaned_data['agreement_form'] + project_key = form.cleaned_data['project_key'] + participant_email = form.cleaned_data['participant'] + + project = DataProject.objects.get(project_key=project_key) + participant = Participant.objects.get(project=project, user__email=participant_email) + + signed_agreement_form = SignedAgreementForm( + user=participant.user, + agreement_form=agreement_form, + project=project, + date_signed=datetime.now(), + upload=signed_agreement_form, + status=SIGNED_FORM_APPROVED, + ) + signed_agreement_form.save() + + # Create the response. + response = HttpResponse(status=201) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "success", "Signed agreement form successfully uploaded", "thumbs-up" + ) + + # Close the modal + response['X-IC-Script'] += "$('#page-modal').modal('hide');" + + return response diff --git a/app/projects/migrations/0093_agreementform_internal.py b/app/projects/migrations/0093_agreementform_internal.py new file mode 100644 index 00000000..acfe5fe1 --- /dev/null +++ b/app/projects/migrations/0093_agreementform_internal.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2022-08-09 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0092_auto_20220517_1520'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='internal', + field=models.BooleanField(default=False, help_text='Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants'), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 5f84f060..dd982812 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -96,6 +96,7 @@ class AgreementForm(models.Model): type = models.CharField(max_length=50, choices=AGREEMENT_FORM_TYPE, blank=True, null=True) order = models.IntegerField(default=50, help_text="Indicate an order (lowest number = first listing) for how the Agreement Forms should be listed during registration workflows.") content = models.TextField(blank=True, null=True, help_text="If Agreement Form type is set to 'MODEL', the HTML set here will be rendered for the user") + internal = models.BooleanField(default=False, help_text="Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants") def __str__(self): return '%s' % (self.name) diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index 80ccaa7d..2602fe85 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -197,6 +197,10 @@

    Team members

    {{ form.agreement_form.short_name }} {% endfor %} + +
    ").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter); -else if(b.length>0){p.nTFoot=b[0];da(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)", -"g"),Wa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,L=function(a){return!a||!0===a||"-"===a?!0:!1},Nb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Ob=function(a,b){Xa[b]||(Xa[b]=RegExp(Pa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Xa[b],"."):a},Ya=function(a,b,c){var d="string"===typeof a;if(L(a))return!0;b&&d&&(a=Ob(a,b));c&&d&&(a=a.replace(Wa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Pb=function(a,b,c){return L(a)?!0:!(L(a)||"string"=== -typeof a)?null:Ya(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;ea.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d")[0],Wb=va.textContent!==k,Yb=/<.*?>/g,Na=m.util.throttle,Rb=[],w=Array.prototype,ac=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof -h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;ea?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=aa(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&& -c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Tb(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Tb(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){bb(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Ub=function(a,b,c,d,e){for(var c= -[],d=0,f=e.length;d=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Ub(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc):"";if(k)switch(k[2]){case "visIdx":case "visible":b= -parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[Z(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()", -"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Ub,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()", -function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ia(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ia(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m= -h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;id;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot? -h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};m.camelToHungarian=I;o("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments); -a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){na(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a=this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a|| -!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j)); -b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c, -1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,n){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,n)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=Q(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.16";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow= -{nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null, -sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g, -this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+ -"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries", -sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"}; -X(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null, -bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[], -aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null, -aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength, -b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}}, -order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd", -sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead", -sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Kb=m.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ha(a, -b)]},simple_numbers:function(a,b){return["previous",ha(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ha(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ha(a,b),"last"]},_numbers:ha,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},n,l,m=0,o=function(b,d){var k,s,u,r,v=function(b){Sa(a,b.data.action,true)};k=0;for(s=d.length;k").appendTo(b);o(u,r)}else{n=null;l="";switch(r){case "ellipsis":b.append('');break;case "first":n=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":n=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":n=j.sNext;l=r+(e",{"class":g.sPageButton+ -" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(n).appendTo(b);Va(u,{action:r},v);m++}}}},s;try{s=h(b).find(G.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+s+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a); -return null!==b&&!isNaN(b)||L(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c,!0)?"html-num-fmt"+c:null},function(a){return L(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return L(a)?a:"string"===typeof a?a.replace(Mb," ").replace(Aa,""):""},string:function(a){return L(a)? -a:"string"===typeof a?a.replace(Mb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Ob(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return L(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return L(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a< -b?-1:a>b?1:0},"string-desc":function(a,b){return ab?-1:0}});cb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("
    ").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b); -h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Vb=function(a){return"string"===typeof a?a.replace(//g,">").replace(/"/g, -"""):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Vb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Vb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Lb,_fnBuildAjax:sa,_fnAjaxUpdate:kb,_fnAjaxParameters:tb,_fnAjaxUpdateDraw:ub, -_fnAjaxDataSrc:ta,_fnAddColumn:Da,_fnColumnOptions:ja,_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:Z,_fnColumnIndexToVisible:$,_fnVisbleColumns:aa,_fnGetColumns:la,_fnColumnTypes:Fa,_fnApplyColumnDefs:hb,_fnHungarianMap:X,_fnCamelToHungarian:I,_fnLanguageCompat:Ca,_fnBrowserDetect:fb,_fnAddData:M,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:ib, -_fnSplitObjNotation:Ia,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:R,_fnGetDataMaster:Ja,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ca,_fnGetRowElements:Ha,_fnCreateTr:Ga,_fnBuildHead:jb,_fnDrawHead:ea,_fnDraw:N,_fnReDraw:S,_fnAddOptionsHtml:mb,_fnDetectHeader:da,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:ob,_fnFilterComplete:fa,_fnFilterCustom:xb,_fnFilterColumn:wb,_fnFilter:vb,_fnFilterCreateSearch:Oa,_fnEscapeRegex:Pa,_fnFilterData:yb,_fnFeatureHtmlInfo:rb,_fnUpdateInfo:Bb,_fnInfoMacros:Cb,_fnInitialise:ga, -_fnInitComplete:ua,_fnLengthChange:Qa,_fnFeatureHtmlLength:nb,_fnFeatureHtmlPaginate:sb,_fnPageChange:Sa,_fnFeatureHtmlProcessing:pb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:qb,_fnScrollDraw:ka,_fnApplyToChildren:H,_fnCalculateColumnWidths:Ea,_fnThrottle:Na,_fnConvertToWidth:Db,_fnGetWidestNode:Eb,_fnGetMaxLenString:Fb,_fnStringToCss:v,_fnSortFlatten:V,_fnSort:lb,_fnSortAria:Hb,_fnSortListener:Ua,_fnSortAttachListener:La,_fnSortingClasses:wa,_fnSortData:Gb,_fnSaveState:xa,_fnLoadState:Ib,_fnSettingsFromNode:ya, -_fnLog:J,_fnMap:F,_fnBindAction:Va,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Ra,_fnRenderer:Ma,_fnDataSource:y,_fnRowAttributes:Ka,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable}); \ No newline at end of file +/*! DataTables 1.13.2 + * ©2008-2023 SpryMedia Ltd - datatables.net/license + */ +!function(n){"use strict";"function"==typeof define&&define.amd?define(["jquery"],function(t){return n(t,window,document)}):"object"==typeof exports?module.exports=function(t,e){return t=t||window,e=e||("undefined"!=typeof window?require("jquery"):require("jquery")(t)),n(e,t,t.document)}:window.DataTable=n(jQuery,window,document)}(function(P,j,y,N){"use strict";function d(t){var e=parseInt(t,10);return!isNaN(e)&&isFinite(t)?e:null}function l(t,e,n){var a=typeof t,r="string"==a;return"number"==a||"bigint"==a||!!h(t)||(e&&r&&(t=G(t,e)),n&&r&&(t=t.replace(q,"")),!isNaN(parseFloat(t))&&isFinite(t))}function a(t,e,n){var a;return!!h(t)||(h(a=t)||"string"==typeof a)&&!!l(t.replace(V,""),e,n)||null}function m(t,e,n,a){var r=[],o=0,i=e.length;if(a!==N)for(;o").appendTo(l)),h.nTHead=n[0],l.children("tbody")),n=(0===a.length&&(a=P("
    ").insertAfter(n)),h.nTBody=a[0],l.children("tfoot"));if(0===(n=0===n.length&&0").appendTo(l):n).length||0===n.children().length?l.addClass(p.sNoFooter):0/g,X=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,J=new RegExp("(\\"+["/",".","*","+","?","|","(",")","[","]","{","}","\\","$","^","-"].join("|\\")+")","g"),q=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,h=function(t){return!t||!0===t||"-"===t},G=function(t,e){return c[e]||(c[e]=new RegExp(Ot(e),"g")),"string"==typeof t&&"."!==e?t.replace(/\./g,"").replace(c[e],"."):t},H=function(t,e,n){var a=[],r=0,o=t.length;if(n!==N)for(;r").css({position:"fixed",top:0,left:-1*P(j).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(P("
    ").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(P("
    ").css({width:"100%",height:10}))).appendTo("body")).children()).children(),e.barWidth=a[0].offsetWidth-a[0].clientWidth,e.bScrollOversize=100===r[0].offsetWidth&&100!==a[0].clientWidth,e.bScrollbarLeft=1!==Math.round(r.offset().left),e.bBounding=!!n[0].getBoundingClientRect().width,n.remove()),P.extend(t.oBrowser,C.__browser),t.oScroll.iBarWidth=C.__browser.barWidth}function et(t,e,n,a,r,o){var i,l=a,s=!1;for(n!==N&&(i=n,s=!0);l!==r;)t.hasOwnProperty(l)&&(i=s?e(i,t[l],l,t):t[l],s=!0,l+=o);return i}function nt(t,e){var n=C.defaults.column,a=t.aoColumns.length,n=P.extend({},C.models.oColumn,n,{nTh:e||y.createElement("th"),sTitle:n.sTitle||(e?e.innerHTML:""),aDataSort:n.aDataSort||[a],mData:n.mData||a,idx:a}),n=(t.aoColumns.push(n),t.aoPreSearchCols);n[a]=P.extend({},C.models.oSearch,n[a]),at(t,a,P(e).data())}function at(t,e,n){function a(t){return"string"==typeof t&&-1!==t.indexOf("@")}var e=t.aoColumns[e],r=t.oClasses,o=P(e.nTh),i=(!e.sWidthOrig&&(e.sWidthOrig=o.attr("width")||null,u=(o.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/))&&(e.sWidthOrig=u[1]),n!==N&&null!==n&&(Q(n),w(C.defaults.column,n,!0),n.mDataProp===N||n.mData||(n.mData=n.mDataProp),n.sType&&(e._sManualType=n.sType),n.className&&!n.sClass&&(n.sClass=n.className),n.sClass&&o.addClass(n.sClass),u=e.sClass,P.extend(e,n),F(e,n,"sWidth","sWidthOrig"),u!==e.sClass&&(e.sClass=u+" "+e.sClass),n.iDataSort!==N&&(e.aDataSort=[n.iDataSort]),F(e,n,"aDataSort")),e.mData),l=A(i),s=e.mRender?A(e.mRender):null,u=(e._bAttrSrc=P.isPlainObject(i)&&(a(i.sort)||a(i.type)||a(i.filter)),e._setter=null,e.fnGetData=function(t,e,n){var a=l(t,e,N,n);return s&&e?s(a,e,t,n):a},e.fnSetData=function(t,e,n){return b(i)(t,e,n)},"number"!=typeof i&&(t._rowReadObject=!0),t.oFeatures.bSort||(e.bSortable=!1,o.addClass(r.sSortableNone)),-1!==P.inArray("asc",e.asSorting)),n=-1!==P.inArray("desc",e.asSorting);e.bSortable&&(u||n)?u&&!n?(e.sSortingClass=r.sSortableAsc,e.sSortingClassJUI=r.sSortJUIAscAllowed):!u&&n?(e.sSortingClass=r.sSortableDesc,e.sSortingClassJUI=r.sSortJUIDescAllowed):(e.sSortingClass=r.sSortable,e.sSortingClassJUI=r.sSortJUI):(e.sSortingClass=r.sSortableNone,e.sSortingClassJUI="")}function O(t){if(!1!==t.oFeatures.bAutoWidth){var e=t.aoColumns;ee(t);for(var n=0,a=e.length;ne&&t[r]--;-1!=a&&n===N&&t.splice(a,1)}function bt(n,a,t,e){function r(t,e){for(;t.childNodes.length;)t.removeChild(t.firstChild);t.innerHTML=S(n,a,e,"display")}var o,i,l=n.aoData[a];if("dom"!==t&&(t&&"auto"!==t||"dom"!==l.src)){var s=l.anCells;if(s)if(e!==N)r(s[e],e);else for(o=0,i=s.length;o").appendTo(r)),c=0,f=s.length;c=s.fnRecordsDisplay()?0:l,s.iInitDisplayStart=-1);var n=R(t,"aoPreDrawCallback","preDraw",[t]);if(-1!==P.inArray(!1,n))D(t,!1);else{var a=[],r=0,o=t.asStripeClasses,i=o.length,l=t.oLanguage,s="ssp"==E(t),u=t.aiDisplay,n=t._iDisplayStart,c=t.fnDisplayEnd();if(t.bDrawing=!0,t.bDeferLoading)t.bDeferLoading=!1,t.iDraw++,D(t,!1);else if(s){if(!t.bDestroying&&!e)return void xt(t)}else t.iDraw++;if(0!==u.length)for(var f=s?t.aoData.length:c,d=s?0:n;d",{class:i?o[0]:""}).append(P("
    ").addClass(e),P("td",n).addClass(e).html(t)[0].colSpan=T(o),l.push(n[0]))}var l=[];i(e,n),t._details&&t._details.detach(),t._details=P(l),t._detailsShow&&t._details.insertAfter(t.nTr)}function xe(t,e){var n=t.context;if(n.length&&t.length){var a=n[0].aoData[t[0]];if(a._details){(a._detailsShow=e)?(a._details.insertAfter(a.nTr),P(a.nTr).addClass("dt-hasChild")):(a._details.detach(),P(a.nTr).removeClass("dt-hasChild")),R(n[0],null,"childRow",[e,t.row(t[0])]);var s=n[0],r=new B(s),a=".dt.DT_details",e="draw"+a,t="column-sizing"+a,a="destroy"+a,u=s.aoData;if(r.off(e+" "+t+" "+a),H(u,"_details").length>0){r.on(e,function(t,e){if(s!==e)return;r.rows({page:"current"}).eq(0).each(function(t){var e=u[t];if(e._detailsShow)e._details.insertAfter(e.nTr)})});r.on(t,function(t,e,n,a){if(s!==e)return;var r,o=T(e);for(var i=0,l=u.length;it?new B(e[t],this[t]):null},filter:function(t){var e=[];if(o.filter)e=o.filter.call(this,t,this);else for(var n=0,a=this.length;n").appendTo(t);p(u,n)}else{switch(g=null,b=n,a=c.iTabIndex,n){case"ellipsis":t.append('');break;case"first":g=S.sFirst,0===d&&(a=-1,b+=" "+o);break;case"previous":g=S.sPrevious,0===d&&(a=-1,b+=" "+o);break;case"next":g=S.sNext,0!==h&&d!==h-1||(a=-1,b+=" "+o);break;case"last":g=S.sLast,0!==h&&d!==h-1||(a=-1,b+=" "+o);break;default:g=c.fnFormatNumber(n+1),b=d===n?m.sPageButtonActive:""}null!==g&&(u=c.oInit.pagingTag||"a",r=-1!==b.indexOf(o),me(P("<"+u+">",{class:m.sPageButton+" "+b,"aria-controls":c.sTableId,"aria-disabled":r?"true":null,"aria-label":v[n],"aria-role":"link","aria-current":b===m.sPageButtonActive?"page":null,"data-dt-idx":n,tabindex:a,id:0===f&&"string"==typeof n?c.sTableId+"_"+n:null}).html(g).appendTo(t),{action:n},i))}}var g,b,n,m=c.oClasses,S=c.oLanguage.oPaginate,v=c.oLanguage.oAria.paginate||{};try{n=P(t).find(y.activeElement).data("dt-idx")}catch(t){}p(P(t).empty(),e),n!==N&&P(t).find("[data-dt-idx="+n+"]").trigger("focus")}}}),P.extend(C.ext.type.detect,[function(t,e){e=e.oLanguage.sDecimal;return l(t,e)?"num"+e:null},function(t,e){var n;return(!t||t instanceof Date||X.test(t))&&(null!==(n=Date.parse(t))&&!isNaN(n)||h(t))?"date":null},function(t,e){e=e.oLanguage.sDecimal;return l(t,e,!0)?"num-fmt"+e:null},function(t,e){e=e.oLanguage.sDecimal;return a(t,e)?"html-num"+e:null},function(t,e){e=e.oLanguage.sDecimal;return a(t,e,!0)?"html-num-fmt"+e:null},function(t,e){return h(t)||"string"==typeof t&&-1!==t.indexOf("<")?"html":null}]),P.extend(C.ext.type.search,{html:function(t){return h(t)?t:"string"==typeof t?t.replace(U," ").replace(V,""):""},string:function(t){return!h(t)&&"string"==typeof t?t.replace(U," "):t}});function ke(t,e,n,a){var r;return 0===t||t&&"-"!==t?"number"==(r=typeof t)||"bigint"==r?t:+(t=(t=e?G(t,e):t).replace&&(n&&(t=t.replace(n,"")),a)?t.replace(a,""):t):-1/0}function Me(n){P.each({num:function(t){return ke(t,n)},"num-fmt":function(t){return ke(t,n,q)},"html-num":function(t){return ke(t,n,V)},"html-num-fmt":function(t){return ke(t,n,V,q)}},function(t,e){p.type.order[t+n+"-pre"]=e,t.match(/^html\-/)&&(p.type.search[t+n]=p.type.search.html)})}P.extend(p.type.order,{"date-pre":function(t){t=Date.parse(t);return isNaN(t)?-1/0:t},"html-pre":function(t){return h(t)?"":t.replace?t.replace(/<.*?>/g,"").toLowerCase():t+""},"string-pre":function(t){return h(t)?"":"string"==typeof t?t.toLowerCase():t.toString?t.toString():""},"string-asc":function(t,e){return t").addClass(l.sSortJUIWrapper).append(o.contents()).append(P("").addClass(l.sSortIcon+" "+i.sSortingClassJUI)).appendTo(o),P(r.nTable).on("order.dt.DT",function(t,e,n,a){r===e&&(e=i.idx,o.removeClass(l.sSortAsc+" "+l.sSortDesc).addClass("asc"==a[e]?l.sSortAsc:"desc"==a[e]?l.sSortDesc:i.sSortingClass),o.find("span."+l.sSortIcon).removeClass(l.sSortJUIAsc+" "+l.sSortJUIDesc+" "+l.sSortJUI+" "+l.sSortJUIAscAllowed+" "+l.sSortJUIDescAllowed).addClass("asc"==a[e]?l.sSortJUIAsc:"desc"==a[e]?l.sSortJUIDesc:i.sSortingClassJUI))})}}});function We(t){return"string"==typeof(t=Array.isArray(t)?t.join(","):t)?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,"""):t}function Ee(t,e,n,a,r){return j.moment?t[e](r):j.luxon?t[n](r):a?t[a](r):t}var Be=!1;function Ue(t,e,n){var a;if(j.moment){if(!(a=j.moment.utc(t,e,n,!0)).isValid())return null}else if(j.luxon){if(!(a=e&&"string"==typeof t?j.luxon.DateTime.fromFormat(t,e):j.luxon.DateTime.fromISO(t)).isValid)return null;a.setLocale(n)}else e?(Be||alert("DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17"),Be=!0):a=new Date(t);return a}function Ve(s){return function(a,r,o,i){0===arguments.length?(o="en",a=r=null):1===arguments.length?(o="en",r=a,a=null):2===arguments.length&&(o=r,r=a,a=null);var l="datetime-"+r;return C.ext.type.order[l]||(C.ext.type.detect.unshift(function(t){return t===l&&l}),C.ext.type.order[l+"-asc"]=function(t,e){t=t.valueOf(),e=e.valueOf();return t===e?0:t + + @@ -20,10 +22,15 @@ {% endblock %} {% block subcontent %} -
    -
    -

    Essentials

    -
    + +
    @@ -111,9 +118,10 @@

    Agreement forms and downloadable files

    -
    - @@ -278,43 +289,45 @@

    Access management

    {% if project.has_teams %} -
    +
    Name @@ -83,51 +104,63 @@ - \ No newline at end of file diff --git a/app/templates/email/email_new_team_status_notification.html b/app/templates/email/email_new_team_status_notification.html new file mode 100644 index 00000000..4cb13993 --- /dev/null +++ b/app/templates/email/email_new_team_status_notification.html @@ -0,0 +1,24 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Team Status Changed{% endblock %} + +{% block content %} + +

    Hi,

    + +

    This is a notification that a {{ project.name }} challenge administrator has changed your team status to {{ status }}.

    + +

    Team statuses have the following meanings: +

      +
    • Pending - A team can add new members but it is not approved yet to participate.
    • +
    • Ready - A team has all of its members registered and is awaiting approval to participate.
    • +
    • Active - A team has been approved by a challenge administrator to participate.
    • +
    • Deactivated - A team has been blocked by an administrator from any further participation.
    • +
    +

    + +

    You can click here to return to the {{ project.name }} challenge page.

    + +

    Thank you.

    + +{% endblock %} diff --git a/app/templates/email/email_new_team_status_notification.txt b/app/templates/email/email_new_team_status_notification.txt new file mode 100644 index 00000000..c31a6471 --- /dev/null +++ b/app/templates/email/email_new_team_status_notification.txt @@ -0,0 +1,7 @@ +This is a notification that a {{ project.name }} challenge administrator has changed your team status to {{ status }}. + +Team statuses have the following meanings: +- Pending: A team can add new members but it is not approved yet to participate. +- Ready: A team has all of its members registered and is awaiting approval to participate. +- Active: A team has been approved by a challenge administrator to participate. +- Deactivated: A team has been blocked by an administrator from any further participation. \ No newline at end of file diff --git a/app/templates/email/email_team_deleted_notification.html b/app/templates/email/email_team_deleted_notification.html new file mode 100644 index 00000000..c5ed422d --- /dev/null +++ b/app/templates/email/email_team_deleted_notification.html @@ -0,0 +1,21 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Team Deleted{% endblock %} + +{% block content %} + +

    Hi,

    + +

    This is a notification that a {{ project.name }} challenge administrator has deleted your team.

    + +{% if administrator_message %} +

    A contest administrator provided the following reason why:

    + +

    {{ administrator_message }}

    +{% endif %} + +

    You can click here to return to the {{ project.name }} challenge page.

    + +

    Thank you.

    + +{% endblock %} diff --git a/app/templates/email/email_team_deleted_notification.txt b/app/templates/email/email_team_deleted_notification.txt new file mode 100644 index 00000000..e10ef076 --- /dev/null +++ b/app/templates/email/email_team_deleted_notification.txt @@ -0,0 +1,8 @@ +This is a notification that a {{ project.name }} challenge administrator has deleted your team. + +{% if administrator_message %} +A contest administrator provided the following reason why: +"{{ administrator_message }}" +{% endif %} + +Thank you. From 5dfd62d0263162f3f0e1b53106fdcbcd45361bb9 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 8 Mar 2018 09:56:40 -0500 Subject: [PATCH 167/613] TC-177: Do not delete forms on team delete --- app/projects/views_teams.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index fdd4e887..a0b0294b 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -99,11 +99,6 @@ def delete_team(request): sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) - logger.debug('[HYPATIO][delete_team] Deleting all signed forms by team members.') - - for member in team.participant_set.all(): - SignedAgreementForm.objects.filter(user__email=member.user.email, project=project).delete() - logger.debug('[HYPATIO][delete_team] Sending a notification to team members.') # Then send a notification to the team members From e3cd14864add3a7cf9633f074ce199e35285da49 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 8 Mar 2018 10:36:37 -0500 Subject: [PATCH 168/613] TC-177: Clarifies some messages displayed to the user --- app/templates/datacontests/manageteams.html | 2 +- app/templates/datacontests/teamsetup.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 0d05c765..7014b394 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -26,7 +26,7 @@
    {{ team }} {{ team.participant_set.all.count }} @@ -124,23 +125,6 @@

    Challenge Statistics

    - - {% endblock %} {% block footerscripts %} @@ -159,34 +143,11 @@ diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 7014b394..988d9588 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -1,20 +1,33 @@ +{% extends 'base.html' %} {% load countries %} +{% load tz %} +{% block headscripts %} + + + + +{% endblock %} + +{% block tab_name %}Team Management{% endblock %} +{% block title %}{{ project.project_key }} Team Management{% endblock %} +{% block subtitle %}Team leader: {{ team.team_leader }}{% endblock %} + +{% block content %}
    -
    -

    Team Leader: {{ team.team_leader.email }}

    +
    -
    +
    -
    @@ -32,7 +45,7 @@
    - +
    @@ -41,68 +54,152 @@
    - - - - - - - - - - - - - {% for member in team_members %} - - - - - - -
    NameLocationEmailStatusSigned FormsActions
    {{ member.user_info.first_name }} {{ member.user_info.last_name }} - {% get_country member.user_info.country as country %} - {% if country == 'US' and member.user_info.state != "" %} - {{ member.user_info.state }}, USA - {% else %} - {{ country.name }} - {% endif %} - - {{ member.email }} - {% if member.email == team.team_leader.email %} - Team leader - {% elif member.signed_agreement_forms.count != num_required_forms %} - Pending forms - {% elif member.participant.team_pending %} - Pending team leader approval - {% else %} - No issues - {% endif %} - - {% for form in member.signed_agreement_forms %} - - {{ form.agreement_form.short_name }} - +
    +
    +

    Team Members

    +
    +
    + + + + + + + + + + + + + {% for member in team_members %} + + + + + + + + {% endfor %} - - - + +
    NameLocationEmailStatusSigned FormsActions
    {{ member.user_info.first_name }} {{ member.user_info.last_name }} + {% get_country member.user_info.country as country %} + {% if country == 'US' and member.user_info.state != "" %} + {{ member.user_info.state }}, USA + {% else %} + {{ country.name }} + {% endif %} + + {{ member.email }} + {% if member.email == team.team_leader.email %} + Team leader + {% elif member.signed_agreement_forms.count != num_required_forms %} + Pending forms + {% elif member.participant.team_pending %} + Pending team leader approval + {% else %} + No issues + {% endif %} + + {% for form in member.signed_agreement_forms %} + + {{ form.agreement_form.short_name }} + + {% endfor %} + + +
    - -
    +
    +
    + + + +
    +
    +
    +
    +

    Data downloads

    +
    +
    + + + + + + + + + + {% for download in downloads %} + + + + + + {% endfor %} + +
    PersonFileDate
    {{ download.user.email }}{{ download.hosted_file.file_name }}{{ download.download_date|timezone:"America/New_York" }} (EST)
    +
    +
    +
    +
    +
    +
    +

    Admin comments

    +
    +
    +
    + + +
    + + +
    + +

    Previous comments:

    + + {% for comment in comments %} +
    +
    {{ comment.user.email }}, {{ comment.date|timezone:"America/New_York" }} (EST)
    +
    {{ comment.text }}
    +
    {% endfor %} -
    +
    +
    +{% endblock %} + +{% block footerscripts %} + + + \ No newline at end of file + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/project_details.html b/app/templates/project_details.html index fe671a66..e07dea9e 100644 --- a/app/templates/project_details.html +++ b/app/templates/project_details.html @@ -5,6 +5,7 @@ {% block headscripts %} {% endblock %} +{% block tab_name %}{{ project.name }}{% endblock %} {% block title %}{{ project.name }}{% endblock %} {% block subtitle %}{{ project.short_description }}{% endblock %} From e8922fcb782b5f2f0036a08fae136714f908594a Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 9 Mar 2018 12:16:58 -0500 Subject: [PATCH 170/613] TC-170: Lowercase "Members" for panel heading consistency --- app/templates/datacontests/manageteams.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 988d9588..1cf1f348 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -56,7 +56,7 @@
    -

    Team Members

    +

    Team members

    From 4dfc8fef76bc903eedea09a322d425da79032da1 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 9 Mar 2018 12:46:13 -0500 Subject: [PATCH 171/613] TC-170: Fixes bug where data downloads for any team were displaying on one team's page --- app/projects/views.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/projects/views.py b/app/projects/views.py index 56f1866b..62e25b19 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -259,9 +259,10 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m user_jwt = request.COOKIES.get("DBMI_JWT", None) # Collect all the team member information needed - team_members = [] + team_member_details = [] + team_participants = team.participant_set.all() - for member in team.participant_set.all(): + for member in team_participants: email = member.user.email # Make a request to SciReg for a specific person's user information @@ -274,7 +275,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m signed_agreement_forms = SignedAgreementForm.objects.filter(user__email=email, project=project) - team_members.append({ + team_member_details.append({ 'email': email, 'user_info': user_info, 'signed_agreement_forms': signed_agreement_forms, @@ -283,14 +284,17 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m institution = project.institution - comments = TeamComment.objects.filter(team=team).order_by('-date') + # Get the comments made about this team by challenge administrators + comments = TeamComment.objects.filter(team=team) + # Get a history of files downloaded for members of this team files = HostedFile.objects.filter(project=project) - downloads = HostedFileDownload.objects.filter(hosted_file__in=files) + team_users = User.objects.filter(participant__in=team_participants) + downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) return render(request, template_name, context={"project": project, "team": team, - "team_members": team_members, + "team_members": team_member_details, "num_required_forms": num_required_forms, "institution": institution, "comments": comments, From 5a9584f98a99871b8859a32a2cdded61535e2c99 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 9 Mar 2018 12:59:34 -0500 Subject: [PATCH 172/613] TC-170: Adds verification that user has permissions to be on team manage screen Also adds some variables needed for displaying user information in the nav bar. --- app/projects/views.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/projects/views.py b/app/projects/views.py index 62e25b19..ba34ec19 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -252,6 +252,17 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m Populates the team management modal popup on the contest management screen. """ + user = request.user + user_logged_in = True + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, project_key) + + if not is_manager: + logger.debug('[HYPATIO][DEBUG][manage_team] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) + project = DataProject.objects.get(project_key=project_key) team = Team.objects.get(data_project=project, team_leader__email=team_leader) num_required_forms = project.agreement_forms.count() @@ -292,7 +303,11 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m team_users = User.objects.filter(participant__in=team_participants) downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) - return render(request, template_name, context={"project": project, + return render(request, template_name, context={"user_logged_in": user_logged_in, + "user": user, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "project": project, "team": team, "team_members": team_member_details, "num_required_forms": num_required_forms, @@ -316,7 +331,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte is_manager = sciauthz.user_has_manage_permission(request, project_key) if not is_manager: - logger.debug('[HYPATIO][DEBUG] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + logger.debug('[HYPATIO][DEBUG][manage_contest] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') return HttpResponse(403) teams = Team.objects.filter(data_project=project) From 7dde633b9998c1bb15c3e858c68b8e502f7ef152 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 9 Mar 2018 15:22:07 -0500 Subject: [PATCH 173/613] TC-170: Fixes bug where team rows not loaded at first are not clickable This is because the DataTable does not load all rows into DOM at first. --- app/templates/datacontests/managecontests.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index a244ff8c..5819fba3 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -145,7 +145,7 @@

    Challenge Statistics

    diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 5c2ab663..be243e36 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -255,7 +255,7 @@

    Previous comments:

    $.post("/projects/delete_team/", request_data) .done(function() { - location.reload(); + window.close(); }).fail(function() { alert('Failed to delete team.'); }); From a69608ac93f9b19875d88f4d5b31f3f68ab88784 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 13 Mar 2018 14:19:24 -0400 Subject: [PATCH 176/613] TC-179: Accepting and rejecting forms Signed forms now have statuses of either accepted, rejected, or pending. On the challenge management screen, they are highlighted with different colors to indicate their status. Clicking on a signed form brings it up in a new window, with buttons to change its status. When rejecting a form, the user will be prompted to explain why and that message will be sent to the affected person by email. If the team is already in an Active state when this happens, the team will be dropped to a Ready state so it can be reviewed again, and this will remove the team members' VIEW permissions. Also, the button to activate a team will be disabled if any team member has not yet filled in a form or does not have all their forms approved. --- app/projects/admin.py | 2 +- .../0028_signedagreementform_status.py | 20 +++ app/projects/models.py | 7 + app/projects/urls.py | 8 +- app/projects/views.py | 63 ++++++--- app/projects/views_teams.py | 86 +++++++++++++ .../datacontests/managecontests.html | 2 +- app/templates/datacontests/manageteams.html | 28 ++-- .../email_new_team_status_notification.html | 4 + .../email_new_team_status_notification.txt | 2 + ...il_signed_form_rejection_notification.html | 20 +++ ...ail_signed_form_rejection_notification.txt | 7 + .../email_team_deleted_notification.html | 2 +- app/templates/project_details.html | 2 +- app/templates/signed_agreement_form.html | 121 ++++++++++++++++++ 15 files changed, 344 insertions(+), 30 deletions(-) create mode 100644 app/projects/migrations/0028_signedagreementform_status.py create mode 100644 app/templates/email/email_signed_form_rejection_notification.html create mode 100644 app/templates/email/email_signed_form_rejection_notification.txt create mode 100644 app/templates/signed_agreement_form.html diff --git a/app/projects/admin.py b/app/projects/admin.py index d25dd731..ac5132ac 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -16,7 +16,7 @@ class AgreementformAdmin(admin.ModelAdmin): list_display = ('name', 'short_name', 'form_html') class SignedagreementformAdmin(admin.ModelAdmin): - list_display = ('user', 'agreement_form', 'date_signed') + list_display = ('user', 'agreement_form', 'date_signed', 'status') class TeamAdmin(admin.ModelAdmin): list_display = ('team_leader', 'data_project') diff --git a/app/projects/migrations/0028_signedagreementform_status.py b/app/projects/migrations/0028_signedagreementform_status.py new file mode 100644 index 00000000..0bca61dd --- /dev/null +++ b/app/projects/migrations/0028_signedagreementform_status.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-12 17:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0027_teamcomment'), + ] + + operations = [ + migrations.AddField( + model_name='signedagreementform', + name='status', + field=models.CharField(choices=[('P', 'Pending Approval'), ('A', 'Approved'), ('R', 'Rejected')], default='P', max_length=1), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 8b604886..da441d09 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -22,6 +22,12 @@ ('Deactivated', 'Deactivated') ) +SIGNED_FORM_STATUSES = ( + ('P', 'Pending Approval'), + ('A', 'Approved'), + ('R', 'Rejected'), +) + def get_agreement_form_upload_path(instance, filename): form_directory = 'agreementforms/' @@ -102,6 +108,7 @@ class SignedAgreementForm(models.Model): project = models.ForeignKey(DataProject) date_signed = models.DateTimeField(auto_now_add=True) agreement_text = models.TextField(blank=False) + status = models.CharField(max_length=1, null=False, blank=False, default='P', choices=SIGNED_FORM_STATUSES) class Team(models.Model): diff --git a/app/projects/urls.py b/app/projects/urls.py index 62e19976..b0b2034f 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -5,7 +5,7 @@ from .views import request_access from .views import submit_user_permission_request from .views import manage_contest -from .views import download_signed_form +from .views import signed_agreement_form from .views import save_signed_agreement_form from .views import project_details from .views import signout @@ -21,6 +21,8 @@ from .views_teams import change_team_status from .views_teams import delete_team from .views_teams import save_team_comment +from .views_teams import change_signed_form_status +from .views_teams import download_signed_form from .views_files import download_dataset @@ -43,8 +45,10 @@ url(r'^change_team_status/$', change_team_status), url(r'^delete_team/$', delete_team), url(r'^save_team_comment/$', save_team_comment), + url(r'^change_signed_form_status/$', change_signed_form_status), + url(r'^download_signed_form/$', download_signed_form), url(r'^team_signup_form/(P[^/]+)/$', team_signup_form), - url(r'^download_signed_form/$', download_signed_form), + url(r'^signed_agreement_form/$', signed_agreement_form), url(r'^download_dataset/$', download_dataset), url(r'^(?P[^/]+)/$', project_details) ) diff --git a/app/projects/views.py b/app/projects/views.py index ba34ec19..9971bef6 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -90,20 +90,26 @@ def submit_user_permission_request(request): return HttpResponse(200) @user_auth_and_jwt -def download_signed_form(request): - - user_jwt = request.COOKIES.get("DBMI_JWT", None) +def signed_agreement_form(request, template_name='signed_agreement_form.html'): project_key = request.GET['project_key'] signed_agreement_form_id = request.GET['signed_form_id'] + user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(request, project_key) if is_manager: project = get_object_or_404(DataProject, project_key=project_key) - signed_agreement_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) - return HttpResponse(signed_agreement_form.agreement_text, content_type='text/plain') + signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) + participant = Participant.objects.get(data_challenge=project, user=request.user) + + return render(request, template_name, {"user": request.user, + "user_logged_in": True, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "signed_form": signed_form, + "participant": participant}) else: return HttpResponse(403) @@ -255,7 +261,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m user = request.user user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) - + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(request, project_key) @@ -272,6 +278,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m # Collect all the team member information needed team_member_details = [] team_participants = team.participant_set.all() + team_accepted_forms = 0 for member in team_participants: email = member.user.email @@ -283,16 +290,35 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m user_info = user_info_json["results"][0] else: user_info = None - - signed_agreement_forms = SignedAgreementForm.objects.filter(user__email=email, project=project) + + signed_agreement_forms = [] + signed_accepted_agreement_forms = 0 + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + signed_form = SignedAgreementForm.objects.filter(user__email=email, + project=project, + agreement_form=agreement_form).last() + + if signed_form is not None: + signed_agreement_forms.append(signed_form) + + if signed_form.status == 'A': + team_accepted_forms += 1 + signed_accepted_agreement_forms += 1 team_member_details.append({ 'email': email, 'user_info': user_info, 'signed_agreement_forms': signed_agreement_forms, + 'signed_accepted_agreement_forms': signed_accepted_agreement_forms, 'participant': member }) + # Check whether this team has completed all the necessary forms and they have been accepted by challenge admins + total_required_forms_for_team = project.agreement_forms.count() * team_participants.count() + team_has_all_forms_complete = total_required_forms_for_team == team_accepted_forms + institution = project.institution # Get the comments made about this team by challenge administrators @@ -311,6 +337,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m "team": team, "team_members": team_member_details, "num_required_forms": num_required_forms, + "team_has_all_forms_complete": team_has_all_forms_complete, "institution": institution, "comments": comments, "downloads": downloads}) @@ -355,7 +382,11 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte else: user_info = None - signed_agreement_forms = SignedAgreementForm.objects.filter(user__email=email, project=project) + signed_agreement_forms = [] + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + signed_agreement_forms.append(SignedAgreementForm.objects.filter(user__email=email, project=project, agreement_form=agreement_form).last()) users_without_a_team_details.append({ 'email': email, @@ -444,6 +475,7 @@ def project_details(request, project_key, template_name='project_details.html'): # TODO cleanup and eliminate some of these where possible registration_form = None + forms_incomplete = False agreement_forms_list = [] participant = None is_manager = False @@ -520,17 +552,19 @@ def project_details(request, project_key, template_name='project_details.html'): # Order by name descending temporarily so the n2c2 ROC appears before DUA agreement_forms = project.agreement_forms.order_by('-name') - # Check to see if any of the necessary agreement forms have already been signed by the user + # Check to see if any of the agreement forms have been signed and not rejected by an admin for agreement_form in agreement_forms: signed_agreement_form = SignedAgreementForm.objects.filter(project=project, user=user, - agreement_form=agreement_form) + agreement_form=agreement_form, + status__in=["P", "A"]) if signed_agreement_form.count() > 0: already_signed = True else: already_signed = False - + forms_incomplete = True + if current_step is None and not already_signed: current_step = agreement_form.name @@ -556,9 +590,7 @@ def project_details(request, project_key, template_name='project_details.html'): access_granted = True # TODO Temporarily ordering by name descending for n2c2 - # Get all of the files available for this data set - if request.user.is_superuser: data_files = HostedFile.objects.filter(project=project).order_by('-long_name') else: @@ -601,4 +633,5 @@ def project_details(request, project_key, template_name='project_details.html'): "profile_completed": profile_completed, "institution": institution, "registration_form": registration_form, - "current_step": current_step}) + "current_step": current_step, + "forms_incomplete": forms_incomplete}) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index beb2c55b..04554856 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -1,5 +1,7 @@ import logging +from datetime import datetime +from django.shortcuts import get_object_or_404 from django.shortcuts import render from django.shortcuts import redirect from django.http import HttpResponse @@ -21,6 +23,90 @@ logger = logging.getLogger(__name__) + +@user_auth_and_jwt +def download_signed_form(request): + """Returns a text file to the user containing the signed form's content.""" + + form_id = request.GET.get("form_id") + + logger.debug('[HYPATIO][download_signed_form] ' + request.user.email + ' is downloading signed form ' + form_id + '.') + + signed_form = get_object_or_404(SignedAgreementForm, id=form_id) + affected_user = signed_form.user + date_as_string = datetime.strftime(signed_form.date_signed, "%Y%m%d-%H%M") + + filename = affected_user.email + '-' + signed_form.agreement_form.short_name + '-' + date_as_string + '.txt' + response = HttpResponse(signed_form.agreement_text, content_type='text/plain') + response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) + return response + +@user_auth_and_jwt +def change_signed_form_status(request): + """Change a signed form's status and notify the user.""" + + status = request.POST.get("status") + form_id = request.POST.get("form_id") + administrator_message = request.POST.get("administrator_message") + + logger.debug('[HYPATIO][change_signed_form_status] ' + request.user.email + ' changing status for signed form ' + form_id + ' to ' + status + '.') + + signed_form = get_object_or_404(SignedAgreementForm, id=form_id) + affected_user = signed_form.user + + # First change the team's status + if status == "approved": + signed_form.status = 'A' + signed_form.save() + elif status == "rejected": + signed_form.status = 'R' + signed_form.save() + + logger.debug('[HYPATIO][change_signed_form_status] Emailing a rejection notification to the affected participant') + + # Send an email notification to the affected person + context = {'signed_form': signed_form, + 'administrator_message': administrator_message, + 'site_url': settings.SITE_URL} + + email_success = email_send(subject='DBMI Portal Signed Form Rejected', + recipients=[affected_user.email], + email_template='email_signed_form_rejection_notification', + extra=context) + + participant = Participant.objects.get(user=affected_user, data_challenge=signed_form.project) + team = participant.team + + # If the team is in an Active status, move the team status down to Ready and remove everyone's VIEW permissions + if team.status == "Active": + team.status = "Ready" + team.save() + + for member in team.participant_set.all(): + sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz.remove_view_permission(signed_form.project.project_key, member.user.email) + + logger.debug('[HYPATIO][change_signed_form_status] Emailing the whole team that their status has been moved to Ready because someone has a pending form') + + # Send an email notification to the team + context = {'status': "ready", + 'reason': 'Your team has been temporarily disabled because of an issue with a team members\' forms. Challenge administrators will resolve this shortly.', + 'project': signed_form.project, + 'site_url': settings.SITE_URL} + + # Email list + emails = [member.user.email for member in team.participant_set.all()] + + email_success = email_send(subject='DBMI Portal Team Status Changed', + recipients=emails, + email_template='email_new_team_status_notification', + extra=context) + else: + logger.debug('[HYPATIO][change_signed_form_status] Given status "' + status + '" not one of allowed statuses.') + return HttpResponse(500) + + return HttpResponse(200) + @user_auth_and_jwt def save_team_comment(request): """Saves a comment made about a team by a challenge administrator.""" diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index 5819fba3..fe700c95 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -81,7 +81,7 @@

    Pending Participants Without Teams

    - + @@ -86,8 +96,10 @@

    Team members

    - {% endfor %} @@ -233,12 +245,10 @@

    Previous comments:

    }); $('#delete-team').click(function() { - // Display the div containing the confirmation form $('#delete-team-confirmation-form').show(); }); $('#delete-team-cancel').click(function() { - // Display the div containing the confirmation form $('#delete-team-confirmation-form').hide(); }); diff --git a/app/templates/email/email_new_team_status_notification.html b/app/templates/email/email_new_team_status_notification.html index 4cb13993..4ef0b78f 100644 --- a/app/templates/email/email_new_team_status_notification.html +++ b/app/templates/email/email_new_team_status_notification.html @@ -8,6 +8,10 @@

    This is a notification that a {{ project.name }} challenge administrator has changed your team status to {{ status }}.

    +{% if reason %} +

    {{ reason }}

    +{% endif %} +

    Team statuses have the following meanings:

    • Pending - A team can add new members but it is not approved yet to participate.
    • diff --git a/app/templates/email/email_new_team_status_notification.txt b/app/templates/email/email_new_team_status_notification.txt index c31a6471..a298e1bf 100644 --- a/app/templates/email/email_new_team_status_notification.txt +++ b/app/templates/email/email_new_team_status_notification.txt @@ -1,5 +1,7 @@ This is a notification that a {{ project.name }} challenge administrator has changed your team status to {{ status }}. +{% if reason %}{{ reason}}{% endif %} + Team statuses have the following meanings: - Pending: A team can add new members but it is not approved yet to participate. - Ready: A team has all of its members registered and is awaiting approval to participate. diff --git a/app/templates/email/email_signed_form_rejection_notification.html b/app/templates/email/email_signed_form_rejection_notification.html new file mode 100644 index 00000000..fb506cba --- /dev/null +++ b/app/templates/email/email_signed_form_rejection_notification.html @@ -0,0 +1,20 @@ +{% extends "email/email_base.html" %} +{% load tz %} + +{% block title %}DBMI Data Portal - Form Rejected{% endblock %} + +{% block content %} + +

      Hi,

      + +

      This is a notification that a {{ signed_form.project.name }} challenge administrator has rejected the {{ signed_form.agreement_form.name }} form that you signed on {{ signed_form.date_signed|timezone:"America/New_York" }} (EST).

      + +

      A contest administrator provided the following reason why:

      + +

      {{ administrator_message|linebreaks }}

      + +

      If you wish to participate in the challenge, please return to the {{ signed_form.project.name }} challenge page and fill out the form again.

      + +

      Thank you.

      + +{% endblock %} \ No newline at end of file diff --git a/app/templates/email/email_signed_form_rejection_notification.txt b/app/templates/email/email_signed_form_rejection_notification.txt new file mode 100644 index 00000000..5e02185b --- /dev/null +++ b/app/templates/email/email_signed_form_rejection_notification.txt @@ -0,0 +1,7 @@ +This is a notification that a {{ signed_form.project.name }} challenge administrator has rejected the {{ signed_form.agreement_form.name }} form that you signed on {{ signed_form.date_signed }}. A contest administrator provided the following reason why: + +{{ administrator_message }} + +Please return to the {{ signed_form.project.name }} challenge page and fill out the form again if you wish to participate in the challenge. + +Thank you. \ No newline at end of file diff --git a/app/templates/email/email_team_deleted_notification.html b/app/templates/email/email_team_deleted_notification.html index c5ed422d..9a1956c5 100644 --- a/app/templates/email/email_team_deleted_notification.html +++ b/app/templates/email/email_team_deleted_notification.html @@ -11,7 +11,7 @@ {% if administrator_message %}

      A contest administrator provided the following reason why:

      -

      {{ administrator_message }}

      +

      {{ administrator_message|linebreaks }}

      {% endif %}

      You can click here to return to the {{ project.name }} challenge page.

      diff --git a/app/templates/project_details.html b/app/templates/project_details.html index e07dea9e..8a56a7e9 100644 --- a/app/templates/project_details.html +++ b/app/templates/project_details.html @@ -39,7 +39,7 @@

      Description {% else %} {% include 'project_registration_closed.html' %} {% endif %} - {% elif not access_granted %} + {% elif not access_granted or forms_incomplete %} {% if project.registration_open or user.is_superuser %} {% include 'project_signup.html' %} {% else %} diff --git a/app/templates/signed_agreement_form.html b/app/templates/signed_agreement_form.html new file mode 100644 index 00000000..e357e3ee --- /dev/null +++ b/app/templates/signed_agreement_form.html @@ -0,0 +1,121 @@ +{% extends 'base.html' %} +{% load tz %} + +{% block headscripts %} +{% endblock %} + +{% block tab_name %}Signed Agreement Form{% endblock %} +{% block title %}Manage Signed Agreement Form{% endblock %} +{% block subtitle %}{{ signed_form.agreement_form.name }} -- {{ signed_form.user }}{% endblock %} + +{% block content %} +
      +
      +
      + +
      + {{ signed_form.agreement_text|linebreaks }} +
      +
      +
      + +
      +
      + +
      +

      Form Status: {{ signed_form.get_status_display }}

      +

      Signed by: {{ signed_form.user }}

      +

      Member of team: {{ participant.team.team_leader }}

      +

      For Project: {{ signed_form.project.name }}

      +

      Form Name: {{ signed_form.agreement_form.name }}

      +

      Signed on: {{ signed_form.date_signed|timezone:"America/New_York" }} (EST)

      +
      +
      + +
      + +
      +
      +
      + Download + + +
      +
      + +
      +
      +
      +
      +{% endblock %} + +{% block footerscripts %} + +{% endblock %} + From 7a91f83acb638074a6838e7401437eb7950974b2 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 14 Mar 2018 09:43:16 -0400 Subject: [PATCH 177/613] TC-179: Fixes bug where participant record pulled was not the user owning the form --- app/projects/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/projects/views.py b/app/projects/views.py index 9971bef6..e47e74cb 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -102,7 +102,7 @@ def signed_agreement_form(request, template_name='signed_agreement_form.html'): if is_manager: project = get_object_or_404(DataProject, project_key=project_key) signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) - participant = Participant.objects.get(data_challenge=project, user=request.user) + participant = Participant.objects.get(data_challenge=project, user=signed_form.user) return render(request, template_name, {"user": request.user, "user_logged_in": True, From 4cdba6aa4edc87bc0e3e841672f4602ad1d060bf Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 15 Mar 2018 13:13:56 -0400 Subject: [PATCH 178/613] TC-179: Admins should first see if a user is not accepted onto a team before seeing form issues --- app/templates/datacontests/manageteams.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 3f5f98f8..38c49f92 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -99,10 +99,10 @@

      Team members

      Team Leader {% endif %} - {% if member.signed_accepted_agreement_forms != num_required_forms %} - Pending forms - {% elif member.participant.team_pending %} + {% if member.participant.team_pending %} Pending team leader approval + {% elif member.signed_accepted_agreement_forms != num_required_forms %} + Pending forms {% else %} No issues {% endif %} From 5579ca49e0cd441100bc53d10de13d3e979f5de6 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 19 Mar 2018 15:10:33 -0400 Subject: [PATCH 179/613] TC-46: Add ParticipantSubmission model, refactor code, and cleanup project views Refactors some unwieldy code. Removes some redundant variables to minimize the number of variables needed to pass in context to templates. Removes a few template files to cut down on templates within templates within templates. Adds user uploads, team table, and viewing of your own agreement forms to the template that users see when their team is activated. --- app/profile/views.py | 5 +- app/projects/admin.py | 7 +- .../migrations/0029_participantsubmission.py | 25 ++ app/projects/models.py | 35 ++- app/projects/views.py | 264 +++++++--------- app/templates/base.html | 7 +- app/templates/datacontests/teamsetup.html | 20 +- app/templates/dataprojects/blurb.html | 2 +- app/templates/project_compete.html | 32 -- app/templates/project_details.html | 124 -------- app/templates/project_login.html | 11 - app/templates/project_login_or_register.html | 52 ++++ app/templates/project_participate.html | 168 ++++++++++ .../project_registration_closed.html | 5 - app/templates/project_signup.html | 292 ++++++++++++------ app/templates/signed_agreement_form.html | 2 + 16 files changed, 609 insertions(+), 442 deletions(-) create mode 100644 app/projects/migrations/0029_participantsubmission.py delete mode 100644 app/templates/project_compete.html delete mode 100644 app/templates/project_details.html delete mode 100644 app/templates/project_login.html create mode 100644 app/templates/project_login_or_register.html create mode 100644 app/templates/project_participate.html delete mode 100644 app/templates/project_registration_closed.html diff --git a/app/profile/views.py b/app/profile/views.py index 5d1053a3..912e7a59 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -23,7 +23,6 @@ def update_profile(request): user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) # If the JWT has expired or the user doesn't have one, force the user to login again @@ -59,7 +58,6 @@ def update_profile(request): def profile(request, template_name='profile/profile.html'): user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) @@ -90,8 +88,7 @@ def profile(request, template_name='profile/profile.html'): return render(request, template_name, {'registration_form': registration_form, 'user': user, 'is_manager': is_manager, - 'new_user': new_user, - 'user_logged_in': user_logged_in}) + 'new_user': new_user}) def get_client_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') diff --git a/app/projects/admin.py b/app/projects/admin.py index ac5132ac..f5850be4 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -8,6 +8,7 @@ from .models import DataGate from .models import HostedFile from .models import HostedFileDownload +from .models import ParticipantSubmission class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') #, 'project_url') @@ -36,6 +37,9 @@ class HostedFileAdmin(admin.ModelAdmin): class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') +class ParticipantSubmissionAdmin(admin.ModelAdmin): + list_display = {'participant', 'upload_date', 'file'} + admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) admin.site.register(SignedAgreementForm, SignedagreementformAdmin) @@ -44,4 +48,5 @@ class HostedFileDownloadAdmin(admin.ModelAdmin): admin.site.register(Institution, InstitutionAdmin) admin.site.register(DataGate, DataGateAdmin) admin.site.register(HostedFile, HostedFileAdmin) -admin.site.register(HostedFileDownload, HostedFileDownloadAdmin) \ No newline at end of file +admin.site.register(HostedFileDownload, HostedFileDownloadAdmin) +admin.site.register(ParticipantSubmission, ParticipantSubmissionAdmin) diff --git a/app/projects/migrations/0029_participantsubmission.py b/app/projects/migrations/0029_participantsubmission.py new file mode 100644 index 00000000..da7bde50 --- /dev/null +++ b/app/projects/migrations/0029_participantsubmission.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-16 14:17 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0028_signedagreementform_status'), + ] + + operations = [ + migrations.CreateModel( + name='ParticipantSubmission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('upload_date', models.DateTimeField(auto_now_add=True)), + ('file', models.FileField(upload_to='')), + ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Participant')), + ], + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index da441d09..20c446c5 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -90,7 +90,7 @@ class DataProject(models.Model): registration_open = models.BooleanField(default=False, blank=False, null=False) def __str__(self): - return '%s %s' % (self.project_key, self.name) + return '%s' % (self.project_key) class DataGate(models.Model): @@ -112,10 +112,28 @@ class SignedAgreementForm(models.Model): class Team(models.Model): + """ + This model describes a team of participants that are competing in a data challenge. + """ team_leader = models.OneToOneField(User) data_project = models.ForeignKey(DataProject) status = models.CharField(max_length=30, choices=TEAM_STATUS, default='Pending') + def get_count_of_submissions_made(self): + """Returns the total number of submissions that a team's participants have made for its challenge.""" + + submissions = 0 + participants = self.participant_set.all() + for p in participants: + submissions += p.participantsubmission_set.count() + return submissions + + def get_number_of_submissions_left(self): + """Returns the number of submissions left that a team may make.""" + + # TODO: abstract this number to the DataProjects class? + return 3 - self.get_count_of_submissions_made() + def __str__(self): return '%s' % self.team_leader.email @@ -153,6 +171,9 @@ def set_approved(self): self.team_wait_on_leader_email = None self.team_pending = False + def __str__(self): + return '%s - %s' % (self.user, self.data_challenge) + class HostedFile(models.Model): long_name = models.CharField(max_length=100, blank=False, null=False) @@ -166,6 +187,7 @@ class HostedFile(models.Model): def __str__(self): return '%s - %s' % (self.project, self.long_name) + class HostedFileDownload(models.Model): user = models.ForeignKey(User) hosted_file = models.ForeignKey(HostedFile) @@ -180,3 +202,14 @@ class TeamComment(models.Model): def __str__(self): return '%s %s %s' % (self.user, self.team, self.date) + +class ParticipantSubmission(models.Model): + """Captures the files that participants are submitting for their challenges. Through the Participant model + you can get to what team and project this submission pertains to. + """ + participant = models.ForeignKey(Participant) + upload_date = models.DateTimeField(auto_now_add=True) + file = models.FileField() + + def __str__(self): + return '%s %s' % (self.participant.user, self.file) diff --git a/app/projects/views.py b/app/projects/views.py index e47e74cb..f57eb578 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -99,13 +99,12 @@ def signed_agreement_form(request, template_name='signed_agreement_form.html'): sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(request, project_key) - if is_manager: - project = get_object_or_404(DataProject, project_key=project_key) - signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) - participant = Participant.objects.get(data_challenge=project, user=signed_form.user) + project = get_object_or_404(DataProject, project_key=project_key) + signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) + participant = Participant.objects.get(data_challenge=project, user=signed_form.user) + if is_manager or signed_form.user == request.user: return render(request, template_name, {"user": request.user, - "user_logged_in": True, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, "signed_form": signed_form, @@ -126,10 +125,8 @@ def list_data_projects(request, template_name='dataprojects/list.html'): if not request.user.is_authenticated(): user = None - user_logged_in = False else: user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) # If for some reason they have a session but not JWT, force them to log in again. @@ -197,7 +194,6 @@ def list_data_projects(request, template_name='dataprojects/list.html'): data_projects.append(project) return render(request, template_name, {"data_projects": data_projects, - "user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, @@ -214,11 +210,9 @@ def list_data_contests(request, template_name='datacontests/list.html'): if not request.user.is_authenticated(): user = None - user_logged_in = False else: user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) # If for some reason they have a session but not JWT, force them to log in again. @@ -245,7 +239,6 @@ def list_data_contests(request, template_name='datacontests/list.html'): data_contests.append(contest) return render(request, template_name, {"data_contests": data_contests, - "user_logged_in": user_logged_in, "user": user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, @@ -259,7 +252,6 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m """ user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) @@ -329,8 +321,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m team_users = User.objects.filter(participant__in=team_participants) downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) - return render(request, template_name, context={"user_logged_in": user_logged_in, - "user": user, + return render(request, template_name, context={"user": user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, "project": project, @@ -351,7 +342,6 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte user_details = {} user = request.user - user_logged_in = True user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) @@ -404,8 +394,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte institution = project.institution - return render(request, template_name, {"user_logged_in": user_logged_in, - "user": user, + return render(request, template_name, {"user": user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, "project": project, @@ -418,7 +407,6 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte "teams_with_any_submission": teams_with_any_submission, "institution": institution}) -# TODO remove this: activate_team view should now do this @user_auth_and_jwt def grant_access_with_view_permissions(request): @@ -471,167 +459,123 @@ def grant_access_with_view_permissions(request): return HttpResponse(200) @public_user_auth_and_jwt -def project_details(request, project_key, template_name='project_details.html'): +def project_details(request, project_key): + + if not request.user.is_authenticated(): + project = get_object_or_404(DataProject, project_key=project_key, visible=True) + return render(request, 'project_login_or_register.html', {'project': project}) - # TODO cleanup and eliminate some of these where possible registration_form = None - forms_incomplete = False agreement_forms_list = [] - participant = None - is_manager = False - user_requested_access = False - user_access_request_granted = False - email_verified = False - profile_completed = False - team = None - team_members = None - team_has_pending_members = None - user_is_team_leader = False access_granted = False current_step = None - data_files = [] - - if not request.user.is_authenticated(): - user = None - user_logged_in = False - - project = get_object_or_404(DataProject, project_key=project_key, visible=True) + user = request.user + if user.is_superuser: + project = get_object_or_404(DataProject, project_key=project_key) else: - user = request.user - - if user.is_superuser: - project = get_object_or_404(DataProject, project_key=project_key) - else: - project = get_object_or_404(DataProject, project_key=project_key, visible=True) + project = get_object_or_404(DataProject, project_key=project_key, visible=True) - user_logged_in = True - user_jwt = request.COOKIES.get("DBMI_JWT", None) + user_jwt = request.COOKIES.get("DBMI_JWT", None) - # If for some reason they have a session but not JWT, force them to log in again. - if user_jwt is None or validate_jwt(request) is None: - return logout_redirect(request) + # If for some reason they have a session but not JWT, force them to log in again. + if user_jwt is None or validate_jwt(request) is None: + return logout_redirect(request) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission(request, project_key) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(request, project_key) - # The JWT token that will get passed in API calls - jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} + # The JWT token that will get passed in API calls + jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} - # Check for a returning task and set messages accordingly - get_task_context_data(request) + # Check for a returning task and set messages accordingly + get_task_context_data(request) - # Make a request to SciReg to grab email verification and profile information - profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() + # Make a request to SciReg to grab email verification and profile information + profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() - if profile_registration_info['count'] != 0: - profile_registration_info = profile_registration_info["results"][0] - email_verified = profile_registration_info['email_confirmed'] + if profile_registration_info['count'] != 0: + profile_registration_info = profile_registration_info["results"][0] + email_verified = profile_registration_info['email_confirmed'] - # Do not bind the form, otherwise it will present the user with validation markup (green/red stuff) which can be overwhelming - registration_form = RegistrationForm(initial=profile_registration_info) + # Do not bind the form, otherwise it will present the user with validation markup (green/red stuff) which can be overwhelming + registration_form = RegistrationForm(initial=profile_registration_info) - # Check to see if any of the required fields are not populated yet - profile_completed = True - for field in registration_form.fields: - if registration_form.fields[field].required and profile_registration_info[field] == "": - profile_completed = False + # Check to see if any of the required fields are not populated yet + profile_completed = True + for field in registration_form.fields: + if registration_form.fields[field].required and profile_registration_info[field] == "": + profile_completed = False - else: - # User does not have a registration in scireg, present them with a blank form to complete and pre-populate the email - registration_form = RegistrationForm(initial={'email': user.email}, new_registration=True) - email_verified = False - profile_completed = False - - if current_step is None and not email_verified: - current_step = "verify_email" - - if current_step is None and not profile_completed: - current_step = "complete_profile" - - # Order by name descending temporarily so the n2c2 ROC appears before DUA - agreement_forms = project.agreement_forms.order_by('-name') - - # Check to see if any of the agreement forms have been signed and not rejected by an admin - for agreement_form in agreement_forms: - signed_agreement_form = SignedAgreementForm.objects.filter(project=project, - user=user, - agreement_form=agreement_form, - status__in=["P", "A"]) - - if signed_agreement_form.count() > 0: - already_signed = True - else: - already_signed = False - forms_incomplete = True - - if current_step is None and not already_signed: - current_step = agreement_form.name - - agreement_forms_list.append({'agreement_form_name': agreement_form.name, - 'agreement_form_id': agreement_form.id, - 'agreement_form_file': agreement_form.form_html.name, - 'already_signed': already_signed}) - - try: - participant = Participant.objects.get(user=user) - team = participant.team - except ObjectDoesNotExist: - participant = None - team = None - - if participant and team: - team_members = Participant.objects.filter(team=team) - team_has_pending_members = team_members.filter(team_approved=False) - user_is_team_leader = team.team_leader == request.user - - if team and team.status == 'Active': - # Used to display actions after the setup phase such as data downloads and uploads - access_granted = True - - # TODO Temporarily ordering by name descending for n2c2 - # Get all of the files available for this data set - if request.user.is_superuser: - data_files = HostedFile.objects.filter(project=project).order_by('-long_name') - else: - data_files = HostedFile.objects.filter(project=project, enabled=True).order_by('-long_name') - - # If all other steps completed, then last step will be team - if current_step is None: - current_step = "team" + else: + # User does not have a registration in scireg, present them with a blank form to complete and pre-populate the email + registration_form = RegistrationForm(initial={'email': user.email}, new_registration=True) + email_verified = False + profile_completed = False - # Get all of the user's permission requests - access_requests_url = settings.AUTHORIZATION_REQUEST_URL + "?email=" + user.email - logger.debug('[HYPATIO][DEBUG] access_requests_url: ' + access_requests_url) + if current_step is None and not email_verified: + current_step = "verify_email" - user_access_requests = requests.get(access_requests_url, headers=jwt_headers, verify=False).json() - logger.debug('[HYPATIO][DEBUG] User Permission Requests: ' + json.dumps(user_access_requests)) + if current_step is None and not profile_completed: + current_step = "complete_profile" - if user_access_requests is not None and 'results' in user_access_requests: - for access_request in user_access_requests['results']: - if access_request['item'] == project_key: - user_requested_access = True - user_access_request_granted = access_request['request_granted'] - + # Order by name descending temporarily so the n2c2 ROC appears before DUA + agreement_forms = project.agreement_forms.order_by('-name') - institution = project.institution + # Check to see if any of the agreement forms have been signed and not rejected by an admin + for agreement_form in agreement_forms: + signed_agreement_forms = SignedAgreementForm.objects.filter(project=project, + user=user, + agreement_form=agreement_form, + status__in=["P", "A"]) - return render(request, template_name, {"project": project, - "agreement_forms_list": agreement_forms_list, - "participant": participant, - "team": team, - "user_is_team_leader": user_is_team_leader, - "team_members": team_members, - "team_has_pending_members": team_has_pending_members, - "access_granted": access_granted, - "data_files": data_files, - "is_manager": is_manager, - "user_logged_in": user_logged_in, - "user_requested_access": user_requested_access, - "user_access_request_granted": user_access_request_granted, - "email_verified": email_verified, - "profile_completed": profile_completed, - "institution": institution, - "registration_form": registration_form, - "current_step": current_step, - "forms_incomplete": forms_incomplete}) + if signed_agreement_forms.count() > 0: + already_signed = True + else: + already_signed = False + + if current_step is None and not already_signed: + current_step = agreement_form.name + + agreement_forms_list.append({'agreement_form_name': agreement_form.name, + 'agreement_form_id': agreement_form.id, + 'agreement_form_file': agreement_form.form_html.name, + 'already_signed': already_signed}) + + try: + participant = Participant.objects.get(user=user) + team = participant.team + except ObjectDoesNotExist: + participant = None + team = None + + team_has_pending_members = Participant.objects.filter(team=team, team_approved=False) + + # A user has been granted access to a project when they are on an Active team + access_granted = team and team.status == 'Active' + + # If all other steps completed, then last step will be team + if current_step is None: + current_step = "team" + + final_signed_agreement_forms = SignedAgreementForm.objects.filter(project=project, + user=user, + status__in=["P", "A"]) + + context = {"project": project, + "agreement_forms_list": agreement_forms_list, + "participant": participant, + "team": team, + "team_has_pending_members": team_has_pending_members, + "is_manager": is_manager, + "email_verified": email_verified, + "profile_completed": profile_completed, + "institution": project.institution, + "registration_form": registration_form, + "current_step": current_step, + "final_signed_agreement_forms": final_signed_agreement_forms} + + if not access_granted: + return render(request, 'project_signup.html', context) + else: + return render(request, 'project_participate.html', context) diff --git a/app/templates/base.html b/app/templates/base.html index f947aa36..77afc3b8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -167,7 +167,7 @@

      -
      +
      {% modal_contact_form_link 'Need help? Contact us!' %}
      +
      +
      {% block extrainfo %}{% endblock %}
      +
      diff --git a/app/templates/datacontests/teamsetup.html b/app/templates/datacontests/teamsetup.html index fbe9d359..eb9961ef 100644 --- a/app/templates/datacontests/teamsetup.html +++ b/app/templates/datacontests/teamsetup.html @@ -1,26 +1,28 @@ -{% if user_is_team_leader %} +{% if team.team_leader == user %} {% if team.status == "Active" %} - {% elif team.status == "Ready" %} - {% elif team.status == "Deactivated" %} - {% elif team_has_pending_members %} + {% elif team.status == "Ready" %} - {% else %} + {% else %} {% endif %} + {% if team_has_pending_members %} + + {% endif %} +
    {{ person.email }} {% for form in person.signed_agreement_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index 5c2ab663..3f5f98f8 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -23,7 +23,7 @@
    - +
    @@ -34,6 +34,16 @@
    +{% if not team_has_all_forms_complete %} +
    +
    +
    +

    Team cannot be activated until all participants have completed their forms and they have each been approved.

    +
    +
    +
    +{% endif %} +
    Email Status Signed FormsActions
    {{ member.email }} {% if member.email == team.team_leader.email %} - Team leader - {% elif member.signed_agreement_forms.count != num_required_forms %} + Team Leader + {% endif %} + + {% if member.signed_accepted_agreement_forms != num_required_forms %} Pending forms {% elif member.participant.team_pending %} Pending team leader approval @@ -97,12 +109,12 @@

    Team members

    {% for form in member.signed_agreement_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} +
    @@ -29,7 +31,7 @@ - {% for participant in team_members %} + {% for participant in team.participant_set.all %} - {% for participant in team.participant_set.all %} + {% for participant in step.team.participant_set.all %}
    {{ participant.user.email }} diff --git a/app/templates/dataprojects/blurb.html b/app/templates/dataprojects/blurb.html index 421f82ec..89d2bba1 100644 --- a/app/templates/dataprojects/blurb.html +++ b/app/templates/dataprojects/blurb.html @@ -18,7 +18,7 @@

    {{ data_project.description }} {% endautoescape %} - {% if not user_logged_in %} + {% if not user.is_authenticated %}
    Please Register or Sign in before attempting to access this data resource. diff --git a/app/templates/project_compete.html b/app/templates/project_compete.html deleted file mode 100644 index ca9e777a..00000000 --- a/app/templates/project_compete.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - -
    - - {% for file in data_files %} -
    - - {% if file.enabled or user.is_superuser %} -
    -

    {{ file.description }}

    - -
    - - Download Data -
    - {% endif %} -
    - {% endfor %} - -
    \ No newline at end of file diff --git a/app/templates/project_details.html b/app/templates/project_details.html deleted file mode 100644 index 8a56a7e9..00000000 --- a/app/templates/project_details.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends 'base.html' %} -{% load projects_extras %} -{% load bootstrap3 %} - -{% block headscripts %} -{% endblock %} - -{% block tab_name %}{{ project.name }}{% endblock %} -{% block title %}{{ project.name }}{% endblock %} -{% block subtitle %}{{ project.short_description }}{% endblock %} - -{% block content %} - -{% if messages %} -{% include 'messages.html' %} -{% endif %} - -
    - -
    -
    - -
    - {% autoescape off %} - {{ project.description }} - {% endautoescape %} -
    -
    -
    - -
    - - {% if not user_logged_in %} - {% if project.registration_open or user.is_superuser %} - {% include 'project_login.html' %} - {% else %} - {% include 'project_registration_closed.html' %} - {% endif %} - {% elif not access_granted or forms_incomplete %} - {% if project.registration_open or user.is_superuser %} - {% include 'project_signup.html' %} - {% else %} - {% include 'project_registration_closed.html' %} - {% endif %} - {% elif access_granted %} - {% include 'project_compete.html' %} - {% endif %} - -
    -
    -{% endblock %} - -{% block footerscripts %} - - - -{% endblock %} \ No newline at end of file diff --git a/app/templates/project_login.html b/app/templates/project_login.html deleted file mode 100644 index f68e0acf..00000000 --- a/app/templates/project_login.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load projects_extras %} - -{% if project.is_contest %} - -{% else %} - -{% endif %} \ No newline at end of file diff --git a/app/templates/project_login_or_register.html b/app/templates/project_login_or_register.html new file mode 100644 index 00000000..c0979cdb --- /dev/null +++ b/app/templates/project_login_or_register.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} +{% load projects_extras %} +{% load bootstrap3 %} + +{% block headscripts %} +{% endblock %} + +{% block tab_name %}{{ project.name }}{% endblock %} +{% block title %}{{ project.name }}{% endblock %} +{% block subtitle %}{{ project.short_description }}{% endblock %} + +{% block content %} + +{% if messages %} +{% include 'messages.html' %} +{% endif %} + +
    +
    + {% if project.registration_open %} + {% if project.is_contest %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + +
    + +
    + {% autoescape off %} + {{ project.description }} + {% endautoescape %} +
    +
    +
    +
    +{% endblock %} + +{% block footerscripts %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html new file mode 100644 index 00000000..997738ca --- /dev/null +++ b/app/templates/project_participate.html @@ -0,0 +1,168 @@ +{% extends 'base.html' %} +{% load projects_extras %} +{% load bootstrap3 %} +{% load tz %} + +{% block headscripts %} +{% endblock %} + +{% block tab_name %}{{ team.data_project.name }}{% endblock %} +{% block title %}{{ team.data_project.name }}{% endblock %} +{% block subtitle %}{{ team.data_project.short_description }}{% endblock %} + +{% block extrainfo %} + +{% endblock %} + +{% block content %} + +{% if messages %} +{% include 'messages.html' %} +{% endif %} + +
    +
    + +
    + +
    + {% autoescape off %} + {{ team.data_project.description }} + {% endautoescape %} +
    +
    + +
    + +
    + + + + + + + + + {% for participant in team.participant_set.all %} + + + + + {% endfor %} + +
    EmailSubmissions
    + {{ participant.user.email }} + {% if team.team_leader == participant.user %} +   Team leader + {% endif %} + + {{ participant.participantsubmission_set.all|length }} +
    +
    +
    + +
    + +
    + {% for agreement_form in final_signed_agreement_forms %} +

    {{ agreement_form.agreement_form.name }}

    +
    +
    + Signed {{ agreement_form.date_signed|timezone:"America/New_York" }} (EST) +
    +
    + View +
    +
    + {% endfor %} +
    +
    + +
    + +
    + +
    + +
    + + + {% for file in project.hostedfile_set.all %} + {% if file.enabled or user.is_superuser %} +
    +

    {{ file.long_name }}

    +
    +
    + {{ file.description }} +
    +
    + Download +
    +
    + {% endif %} + {% endfor %} +
    +
    + +
    + +
    + {% if team.get_number_of_submissions_left == 0 %} + Your team has used up all of its available submissions. + {% else %} + + +
    Your team has {{ team.get_number_of_submissions_left }} submissions left.
    + +
    + + +
    + {% endif %} +
    +
    +
    +
    +{% endblock %} + +{% block footerscripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/project_registration_closed.html b/app/templates/project_registration_closed.html deleted file mode 100644 index 71c7c931..00000000 --- a/app/templates/project_registration_closed.html +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/templates/project_signup.html b/app/templates/project_signup.html index 48dc9586..8edb0bab 100644 --- a/app/templates/project_signup.html +++ b/app/templates/project_signup.html @@ -1,108 +1,216 @@ +{% extends 'base.html' %} {% load projects_extras %} +{% load bootstrap3 %} -{% if project.is_contest %} - -{% else %} - -{% endif %} +{% block headscripts %} +{% endblock %} -
    +{% block tab_name %}{{ project.name }}{% endblock %} +{% block title %}{{ project.name }}{% endblock %} +{% block subtitle %}{{ project.short_description }}{% endblock %} - -
    - - {% if current_step == 'verify_email' %} -
    - Your primary email on record needs to be verified. Please look in your inbox for the verification email or click below to send another one: - {% include 'profile/verify_email.html' %} +{% block content %} + +{% if messages %} +{% include 'messages.html' %} +{% endif %} + +
    + +
    +
    + +
    + {% autoescape off %} + {{ project.description }} + {% endautoescape %} +
    - {% endif %}
    - -
    - - {% if current_step == 'complete_profile' %} -
    -

    There are a few required fields in your profile that need to be filled out. Please complete the profile registration form below, specifically the fields marked as required. You can go back and edit these fields any time by visiting your profile page.

    + {% else %} + + {% endif %} + +
    + + +
    + + {% if current_step == 'verify_email' %} +
    + Your primary email on record needs to be verified. Please look in your inbox for the verification email or click below to send another one: + {% include 'profile/verify_email.html' %} +
    + {% endif %} +
    + + +
    + + {% if current_step == 'complete_profile' %} +
    +

    There are a few required fields in your profile that need to be filled out. Please complete the profile registration form below, specifically the fields marked as required. You can go back and edit these fields any time by visiting your profile page.

    + + {% include 'profile/registration_form.html' %} +
    + {% endif %} +
    + + + {% for agreement_form in agreement_forms_list %} +
    + + {% if current_step == agreement_form.agreement_form_name %} +
    + {% if not agreement_form.already_signed %} +
    +
    + {{ agreement_form.agreement_form_file|get_agreement_form_file_contents | safe }} +
    + + + + + +
    + +
    + +
    + + {% csrf_token %} +
    + {% endif %} +
    + {% endif %} +
    + {% endfor %} - {% include 'profile/registration_form.html' %} -
    + + {% if project.is_contest %} +
    + + {% if current_step == 'team' %} +
    + {% include "datacontests/teamsetup.html" %} +
    + {% endif %} +
    + {% endif %} +
    + {% else %} + {% endif %} +
    +
    +{% endblock %} - - {% for agreement_form in agreement_forms_list %} -
    - - {% if current_step == agreement_form.agreement_form_name %} -
    - {% if not agreement_form.already_signed %} -
    -
    - {{ agreement_form.agreement_form_file|get_agreement_form_file_contents | safe }} -
    +{% block footerscripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/signed_agreement_form.html b/app/templates/signed_agreement_form.html index e357e3ee..e6bad4ea 100644 --- a/app/templates/signed_agreement_form.html +++ b/app/templates/signed_agreement_form.html @@ -36,6 +36,7 @@

    Information

    + {% if is_manager %}
    + {% endif %}
    {% endblock %} From b20271d502266953a819292d50a3c09ab89b664c Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 22 Mar 2018 10:01:30 -0400 Subject: [PATCH 180/613] TC-46: Refine access_granted variable to check that the participant is approved team member. --- app/projects/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/projects/views.py b/app/projects/views.py index f57eb578..a7cb0e03 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -545,15 +545,13 @@ def project_details(request, project_key): try: participant = Participant.objects.get(user=user) team = participant.team + access_granted = participant.team_approved and team.status == 'Active' except ObjectDoesNotExist: participant = None team = None + access_granted = False team_has_pending_members = Participant.objects.filter(team=team, team_approved=False) - - # A user has been granted access to a project when they are on an Active team - access_granted = team and team.status == 'Active' - # If all other steps completed, then last step will be team if current_step is None: current_step = "team" From 383a687fb45122a1a78399239d26ba1b309f9fec Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 22 Mar 2018 10:10:48 -0400 Subject: [PATCH 181/613] TC-46: Clarifies wthat 403 means when a user receives it on a download they have no access to. --- app/projects/views_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/projects/views_files.py b/app/projects/views_files.py index cd102c02..65423993 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -29,7 +29,7 @@ def download_dataset(request): if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): logger.debug("[views_files][download_dataset] - No Access for user " + request.user.email) - return HttpResponse(403) + return HttpResponse("403 Forbidden. You do not have access to download this file.") file_id = request.GET.get("file_id") file_to_download = get_object_or_404(HostedFile, id=file_id) From 0eb6e57b12ed7536fc38fe05d3c8f53bb941a611 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 22 Mar 2018 10:22:28 -0400 Subject: [PATCH 182/613] TC-46: Adds a boolean field to DataProject to flag if submissions are being accepted --- app/projects/admin.py | 2 +- ..._dataproject_accepting_user_submissions.py | 20 +++++++++++++++++++ app/projects/models.py | 1 + app/templates/project_participate.html | 2 ++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 app/projects/migrations/0030_dataproject_accepting_user_submissions.py diff --git a/app/projects/admin.py b/app/projects/admin.py index f5850be4..ca6aa6ea 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -38,7 +38,7 @@ class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') class ParticipantSubmissionAdmin(admin.ModelAdmin): - list_display = {'participant', 'upload_date', 'file'} + list_display = ('participant', 'upload_date', 'file') admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) diff --git a/app/projects/migrations/0030_dataproject_accepting_user_submissions.py b/app/projects/migrations/0030_dataproject_accepting_user_submissions.py new file mode 100644 index 00000000..a0d8b369 --- /dev/null +++ b/app/projects/migrations/0030_dataproject_accepting_user_submissions.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-22 14:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0029_participantsubmission'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='accepting_user_submissions', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 20c446c5..836b7b02 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -88,6 +88,7 @@ class DataProject(models.Model): is_contest = models.BooleanField(default=False, blank=False, null=False) visible = models.BooleanField(default=False, blank=False, null=False) registration_open = models.BooleanField(default=False, blank=False, null=False) + accepting_user_submissions = models.BooleanField(default=False, blank=False, null=False) def __str__(self): return '%s' % (self.project_key) diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index 997738ca..cbbf0184 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -116,6 +116,7 @@

    {{ file.long_name }}

    + {% if project.accepting_user_submissions %}
    + {% endif %}
    {% endblock %} From f1d73bef462268241f019b8b1a4b8b92fc98d83d Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 9 Apr 2018 10:13:47 -0400 Subject: [PATCH 183/613] TC-186 - Implemented dataset uploading; updated models to store submissions --- app/hypatio/file_services.py | 243 ++++++++++++++++++ app/hypatio/settings.py | 14 + app/manage.py | 0 app/projects/admin.py | 2 +- .../migrations/0031_auto_20180409_1353.py | 33 +++ app/projects/models.py | 3 +- app/projects/urls.py | 5 +- app/projects/views_files.py | 112 +++++++- app/templates/base.html | 40 +++ app/templates/project_participate.html | 221 +++++++++++++++- gunicorn-nginx-entry.sh | 6 + 11 files changed, 655 insertions(+), 24 deletions(-) mode change 100755 => 100644 app/manage.py create mode 100644 app/projects/migrations/0031_auto_20180409_1353.py diff --git a/app/hypatio/file_services.py b/app/hypatio/file_services.py index 670c7e2b..2a931efb 100644 --- a/app/hypatio/file_services.py +++ b/app/hypatio/file_services.py @@ -1,5 +1,6 @@ import boto3 import requests +import furl from botocore.client import Config from django.conf import settings @@ -7,6 +8,248 @@ logger = logging.getLogger(__name__) +def build_url(base, path): + + # Start with the base. + url = furl.furl(base) + + # Get current segments, leaving out blanks + segments = list(filter(None, url.path.segments)) + + # Split the path, leaving out blanks + segments.extend(list(filter(None, path.split('/')))) + + # Add a trailing item to ensure we have a trailing slash + segments.append('') + + url.path.segments = segments + + return url.url + + +def headers(request): + + # Find the JWT. + token = request.META.get('HTTP_AUTHORIZATION', 'SERVICE {}'.format(settings.FILESERVICE_SERVICE_TOKEN)) + + return {"Authorization": token, + 'Content-Type': 'application/json'} + + +def get(request, path, params=None): + logger.debug('Path: {}'.format(path)) + + try: + # Build the url. + url = build_url(settings.FILESERVICE_API_URL, path) + + # Prepare the request. + response = requests.get(url, headers=headers(request), params=params) + + logger.debug('URL: {}, Response: {}'.format(url, response.status_code)) + + response.raise_for_status() + + return response.json() + + except Exception as e: + logger.exception('Exception: {}'.format(e)) + + return None + + +def post(request, path, data): + logger.debug('Path: {}'.format(path)) + + # Build the url. + url = build_url(settings.FILESERVICE_API_URL, path) + + try: + # Prepare the request. + response = requests.post(url, headers=headers(request), json=data) + + logger.debug('URL: {}, Response: {}'.format(url, response.status_code)) + + response.raise_for_status() + + return response.json() + + except Exception as e: + logger.exception('Exception: {}'.format(e)) + + return None + + +def put(request, path, data): + logger.debug('Path: {}'.format(path)) + + # Build the url. + url = build_url(settings.FILESERVICE_API_URL, path) + + try: + + # Prepare the request. + response = requests.put(url, headers=headers(request), json=data) + + logger.debug('URL: {}, Response: {}'.format(url, response.status_code)) + + response.raise_for_status() + + return response + + except Exception as e: + logger.exception('Exception: {}'.format(e)) + + return False + + +def patch(request, path, data): + logger.debug('Path: {}'.format(path)) + + # Build the url. + url = build_url(settings.FILESERVICE_API_URL, path) + + try: + + # Prepare the request. + response = requests.patch(url, headers=headers(request), json=data) + + logger.debug('URL: {}, Response: {}'.format(url, response.status_code)) + + response.raise_for_status() + + return response + + except Exception as e: + logger.exception('Exception: {}'.format(e)) + + return False + + +def delete(request, path, params=None): + logger.debug('Path: {}'.format(path)) + + # Build the url. + url = build_url(settings.FILESERVICE_API_URL, path) + + try: + + # Prepare the request. + response = requests.delete(url, headers=headers(request), params=params) + + logger.debug('URL: {}, Response: {}'.format(url, response.status_code)) + + response.raise_for_status() + + return response + + except Exception as e: + logger.exception('Exception: {}'.format(e)) + + return False + + +def check_groups(request): + + # Get current groups. + groups = get(request, 'groups') + if groups is None: + logger.error('Getting groups failed') + return False + + # Check for the required group. + for group in groups: + if group_name('uploaders') == group['name']: + return True + + # Group was not found, create it. + data = { + 'name': settings.FILESERVICE_GROUP, + 'users': [{'email': settings.FILESERVICE_SERVICE_ACCOUNT}], + } + + # Make the request. + response = post(request, 'groups', data) + if response is None: + logger.error('Failed to create groups: {}'.format(response)) + return False + + # Get the upload group ID. + upload_group_id = [group['id'] for group in response if group['name'] == group_name('UPLOADERS')][0] + + # Create the request to add the bucket to the upload group. + bucket_data = { + 'buckets': [ + {'name': settings.FILESERVICE_AWS_BUCKET} + ] + } + + # Make the request. + response = put(request, '/groups/{}/'.format(upload_group_id), bucket_data) + + return response + + +def create_file(request, filename, metadata, tags=[]): + + # Ensure groups exist. + if not check_groups(request): + logger.error('Groups do not exist or failed to create') + return None + + # Build the request. + data = { + 'permissions': [ + settings.FILESERVICE_GROUP + ], + 'metadata': metadata, + 'filename': filename, + 'tags': tags, + } + + # Make the request. + file = post(request, '/api/file/', data) + + # Get the UUID. + uuid = file['uuid'] + + # Form the request for the file link + params = { + 'cloud': 'aws', + 'bucket': settings.FILESERVICE_AWS_BUCKET, + } + + # Make the request for an s3 presigned post. + response = get(request, '/api/file/{}/post/'.format(uuid), params) + + return uuid, response + + +def uploaded_file(request, uuid, location_id): + + # Build the request. + params = { + 'location': location_id + } + + # Make the request. + response = get(request, '/api/file/{}/uploadcomplete/'.format(uuid)) + + return response is not None + + +def download_file(request, uuid): + + # Make the request. + response = get(request, '/api/file/{}/download/'.format(uuid)) + + return response['url'] + + +def group_name(permission): + return '{}__{}'.format(settings.FILESERVICE_GROUP, permission.upper()) + + def _s3_client(): # Get the service client with sigv4 configured return boto3.client('s3', diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 64c92be4..d1645fc3 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -218,6 +218,20 @@ EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_PORT = os.environ.get("EMAIL_PORT") + +##################################################################################### +# FileService Configurations +##################################################################################### + +FILESERVICE_API_URL = os.environ.get('FILESERVICE_API_URL') +FILESERVICE_GROUP = os.environ.get('FILESERVICE_GROUP') +FILESERVICE_AWS_BUCKET = os.environ.get('S3_BUCKET') +FILESERVICE_SERVICE_ACCOUNT = 'hypatio' +FILESERVICE_SERVICE_TOKEN = os.environ.get('FILESERVICE_SERVICE_TOKEN') + +##################################################################################### + + LOGGING = { 'version': 1, 'handlers': { diff --git a/app/manage.py b/app/manage.py old mode 100755 new mode 100644 diff --git a/app/projects/admin.py b/app/projects/admin.py index ca6aa6ea..6b257c81 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -38,7 +38,7 @@ class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') class ParticipantSubmissionAdmin(admin.ModelAdmin): - list_display = ('participant', 'upload_date', 'file') + list_display = ('participant', 'upload_date', 'uuid', 'location') admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) diff --git a/app/projects/migrations/0031_auto_20180409_1353.py b/app/projects/migrations/0031_auto_20180409_1353.py new file mode 100644 index 00000000..48a9bf80 --- /dev/null +++ b/app/projects/migrations/0031_auto_20180409_1353.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-09 13:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0030_dataproject_accepting_user_submissions'), + ] + + operations = [ + migrations.RemoveField( + model_name='participantsubmission', + name='file', + ), + migrations.RemoveField( + model_name='participantsubmission', + name='id', + ), + migrations.AddField( + model_name='participantsubmission', + name='location', + field=models.CharField(default=None, max_length=12), + ), + migrations.AddField( + model_name='participantsubmission', + name='uuid', + field=models.UUIDField(default=None, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 836b7b02..eae4aa0b 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -210,7 +210,8 @@ class ParticipantSubmission(models.Model): """ participant = models.ForeignKey(Participant) upload_date = models.DateTimeField(auto_now_add=True) - file = models.FileField() + uuid = models.UUIDField(null=False, unique=True, primary_key=True, default=None) + location = models.CharField(max_length=12, default=None) def __str__(self): return '%s %s' % (self.participant.user, self.file) diff --git a/app/projects/urls.py b/app/projects/urls.py index b0b2034f..a0034fd9 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -23,8 +23,8 @@ from .views_teams import save_team_comment from .views_teams import change_signed_form_status from .views_teams import download_signed_form - from .views_files import download_dataset +from .views_files import upload_dataset urlpatterns = ( url(r'^$', list_data_projects), @@ -50,5 +50,6 @@ url(r'^team_signup_form/(P[^/]+)/$', team_signup_form), url(r'^signed_agreement_form/$', signed_agreement_form), url(r'^download_dataset/$', download_dataset), - url(r'^(?P[^/]+)/$', project_details) + url(r'^upload_dataset/$', upload_dataset), + url(r'^(?P[^/]+)/$', project_details), ) diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 65423993..5d5d94d6 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -1,24 +1,26 @@ import logging -import boto3 -import botocore +from django.shortcuts import get_object_or_404 from django.conf import settings -from django.http import StreamingHttpResponse -from django.http import HttpResponse +from django.http import QueryDict +from django.core import exceptions from django.shortcuts import redirect -from django.shortcuts import get_object_or_404 -from django.core.files.base import ContentFile +from django.http import JsonResponse, HttpResponse +from pyauth0jwt.auth0authenticate import user_auth_and_jwt +from rest_framework.exceptions import ValidationError from hypatio.sciauthz_services import SciAuthZ from hypatio.file_services import get_download_url +from hypatio import file_services as fileservice from .models import HostedFile from .models import HostedFileDownload - -from pyauth0jwt.auth0authenticate import user_auth_and_jwt +from .models import ParticipantSubmission +from .models import Participant logger = logging.getLogger(__name__) + @user_auth_and_jwt def download_dataset(request): logger.debug("[views_files][download_dataset] - Attempting file download.") @@ -46,4 +48,96 @@ def download_dataset(request): response['Content-Disposition'] = 'attachment' response['filename'] = file_to_download.file_name - return response \ No newline at end of file + return response + + +@user_auth_and_jwt +def upload_dataset(request): + logger.debug('upload_dataset: {}'.format(request.method)) + + # Check method + if request.method == 'POST': + logger.debug('post') + + # Check user permissions + # TODO: Finish this + + # Assembles the form and run validation. + filename = request.POST.get('filename') + project = request.POST.get('project') + if not filename or not project: + logger.error('No filename or no project!') + return HttpResponse('Filename and project are required', status=400) + + # Prepare the metadata + # TODO: Finish this + metadata = { + 'project': project, + 'uploader': request.user.email, + 'type': 'project_submission', + 'app': 'hypatio', + } + + # Get the file link + uuid, response = fileservice.create_file(request, filename, metadata) + + # Get the needed bits. + post = response['post'] + location = response['locationid'] + + # Form the data for the File object. + file = {'uuid': uuid, 'location': location, 'filename': filename} + + logger.debug('File: {}'.format(file)) + + # Build the response + response = { + 'post': post, + 'file': file, + } + + logger.debug('Response: {}'.format(post)) + + return JsonResponse(data=response) + + elif request.method == 'PATCH': + logger.debug('patch') + + # Get the data + data = QueryDict(request.body) + + logger.debug('Data: {}'.format(data)) + + try: + # Get the participant + participant = Participant.objects.get(user=request.user) + + # Create the object and save UUID and location for future downloads + ParticipantSubmission.objects.create( + participant=participant, + uuid=data['uuid'], + location=data['location']) + + # Make the request to FileService. + if not fileservice.uploaded_file(request, data['uuid'], data['location']): + logger.error('[participants][FilesView][post] FileService uploadCompleted failed!') + else: + logger.debug('FileService updated of file upload') + + return HttpResponse(status=200) + + except exceptions.ObjectDoesNotExist as e: + logger.exception(e) + + return HttpResponse(status=404) + + except Exception as e: + logger.exception(e) + + return HttpResponse(status=500) + else: + + return HttpResponse("Invalid method", status=403) + + + diff --git a/app/templates/base.html b/app/templates/base.html index 77afc3b8..4566191a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -129,6 +129,46 @@ }); + // Fetches the CSRF token from cookies for sending along with AJAX requests. + function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); + } + } + }); + + // Add XSRF headers to Intercooler requests + $(document).on('beforeSend.ic', function(evt, elt, data, settings, xhr, requestId) { + + // Add the CSRF token. + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + console.log('Setting XSRF Token: ' + getCookie('csrftoken')); + xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); + } + }); + {% block headscripts %} diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index cbbf0184..c278a451 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -128,16 +128,21 @@

    Uploads

    -
    Your team has {{ team.get_number_of_submissions_left }} submissions left.
    + + {% csrf_token %} - - + Click to browse for a file... + + + + + - {% endif %} + + {% endif %} {% endif %} @@ -152,19 +157,213 @@
    Your team has {{ team.get_number_of_submissions_left }} submissions left. - {% endblock %} \ No newline at end of file diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index ff6be350..b13400f3 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -56,6 +56,12 @@ export S3_AWS_ACCESS_KEY_ID=$(aws ssm get-parameters --names $PS_PATH.s3_aws_acc export S3_AWS_SECRET_ACCESS_KEY=$(aws ssm get-parameters --names $PS_PATH.s3_aws_secret_access_key --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') export S3_BUCKET=$(aws ssm get-parameters --names $PS_PATH.s3_bucket --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +export FILESERVICE_API_URL=$(aws ssm get-parameters --names $PS_PATH.fileservice_api_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +export FILESERVICE_GROUP=$(aws ssm get-parameters --names $PS_PATH.fileservice_group --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') + +export FILESERVICE_SERVICE_ACCOUNT=$(aws ssm get-parameters --names $PS_PATH.fileservice_service_account --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +export FILESERVICE_SERVICE_TOKEN=$(aws ssm get-parameters --names $PS_PATH.fileservice_service_token --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') + SSL_KEY=$(aws ssm get-parameters --names $PS_PATH.ssl_key --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') SSL_CERT_CHAIN1=$(aws ssm get-parameters --names $PS_PATH.ssl_cert_chain1 --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') SSL_CERT_CHAIN2=$(aws ssm get-parameters --names $PS_PATH.ssl_cert_chain2 --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') From 99cc1ba607d3140bcfe93bea9b1e7477a2e68d12 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 9 Apr 2018 10:18:58 -0400 Subject: [PATCH 184/613] TC-186 - Set to disable form after successful upload --- app/templates/project_participate.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index c278a451..ff914913 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -359,10 +359,7 @@
    Your team has {{ team.get_number_of_submissions_left }} submissions left. From 4e409a5255eb344553d67bedcafc00db1c91bb01 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 11 Apr 2018 10:44:57 -0400 Subject: [PATCH 185/613] TC-198: Prevents new participants from joining activated teams Displays an error message when someone tries to join a team that has already been activated by admins. --- app/projects/views_teams.py | 20 +++++++++--- app/templates/messages.html | 52 +++++++++++++++---------------- app/templates/project_signup.html | 2 +- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 04554856..cf907adf 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -6,6 +6,7 @@ from django.shortcuts import redirect from django.http import HttpResponse from django.core.exceptions import ObjectDoesNotExist +from django.contrib import messages from django.contrib.auth.models import User from django.conf import settings @@ -330,9 +331,20 @@ def join_team(request): try: # If this team leader has already created a team, add the person to the team in a pending status team = Team.objects.get(team_leader__email__iexact=team_leader) - participant.team = team - participant.team_pending = True - participant.save() + + # Only allow a new participant to join a team that is still in a pending or ready state + if team.status in ['Pending', 'Ready']: + participant.team = team + participant.team_pending = True + participant.save() + # Otherwise, let them know why they can't join the team + else: + msg = "The team you are trying to join has already been finalized and is not accepting new members. " + \ + "If you would like to join this team, please have the team leader contact the challenge " + \ + "administrators for help." + messages.error(request, msg) + + return redirect('/projects/' + request.POST.get('project_key') + '/') except ObjectDoesNotExist: # If this team leader has not yet created a team, mark the person as waiting participant.team_wait_on_leader_email = team_leader @@ -346,7 +358,7 @@ def join_team(request): 'project': project, 'site_url': settings.SITE_URL} - email_success = email_send(subject='DBMI Portal - Finalized Team', + email_success = email_send(subject='DBMI Portal - Pending Member', recipients=[team_leader], email_template='email_pending_member_notification', extra=context) diff --git a/app/templates/messages.html b/app/templates/messages.html index 80abcf63..15eb2f1c 100644 --- a/app/templates/messages.html +++ b/app/templates/messages.html @@ -2,31 +2,31 @@ {% if messages %} {% endif %} diff --git a/app/templates/project_signup.html b/app/templates/project_signup.html index 8edb0bab..d92e2690 100644 --- a/app/templates/project_signup.html +++ b/app/templates/project_signup.html @@ -12,7 +12,7 @@ {% block content %} {% if messages %} -{% include 'messages.html' %} + {% include 'messages.html' %} {% endif %}
    From e9520c78965e5ecdbfc004ce110a1057a57f15a8 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 11 Apr 2018 11:13:37 -0400 Subject: [PATCH 186/613] TC-199: Adds a VERIFY_REQUESTS settings flag for SSL use in requests --- app/hypatio/sciauthz_services.py | 20 ++++++++++---------- app/hypatio/settings.py | 1 + app/profile/views.py | 6 +++--- app/projects/views.py | 8 ++++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index 8cd2ab07..82173f8f 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -5,6 +5,8 @@ import furl import logging +from django.conf import settings + logger = logging.getLogger(__name__) class SciAuthZ: @@ -13,8 +15,6 @@ class SciAuthZ: CURRENT_USER_EMAIL = None AUTHORIZATION_REQUEST_URL = None - VERIFY_REQUEST = False - def __init__(self, authz_base, jwt, user_email): user_permissions_url = authz_base + "/user_permission/" @@ -46,7 +46,7 @@ def user_has_manage_permission(self, request, item): permissions_url = self.USER_PERMISSIONS_URL + "?item=" + sciauthz_item + "&email=" + self.CURRENT_USER_EMAIL try: - user_permissions = requests.get(permissions_url, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + user_permissions = requests.get(permissions_url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() except JSONDecodeError: user_permissions = None @@ -60,7 +60,7 @@ def user_has_manage_permission(self, request, item): def current_user_permissions(self): try: - user_permissions = requests.get(self.USER_PERMISSIONS_URL + "?email=" + self.CURRENT_USER_EMAIL, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + user_permissions = requests.get(self.USER_PERMISSIONS_URL + "?email=" + self.CURRENT_USER_EMAIL, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() except JSONDecodeError: user_permissions = None @@ -69,7 +69,7 @@ def current_user_permissions(self): def current_user_access_requests(self): try: - user_access_requests = requests.get(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + user_access_requests = requests.get(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() except JSONDecodeError: user_access_requests = None @@ -77,7 +77,7 @@ def current_user_access_requests(self): def current_user_request_access(self, access_request): try: - user_access_request = requests.post(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, data=json.dumps(access_request), verify=self.VERIFY_REQUEST) + user_access_request = requests.post(self.AUTHORIZATION_REQUEST_URL, headers=self.JWT_HEADERS, data=json.dumps(access_request), verify=settings.VERIFY_REQUESTS) except JSONDecodeError: user_access_request = None @@ -89,7 +89,7 @@ def create_profile_permission(self, grantee_email): modified_headers = self.JWT_HEADERS modified_headers['Content-Type'] = 'application/x-www-form-urlencoded' - profile_permission = requests.post(self.CREATE_PROFILE_PERMISSION, headers=modified_headers, data={"grantee_email": grantee_email}, verify=self.VERIFY_REQUEST) + profile_permission = requests.post(self.CREATE_PROFILE_PERMISSION, headers=modified_headers, data={"grantee_email": grantee_email}, verify=settings.VERIFY_REQUESTS) return profile_permission def create_view_permission(self, project, grantee_email): @@ -103,7 +103,7 @@ def create_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=self.VERIFY_REQUEST) + view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) return view_permission def remove_view_permission(self, project, grantee_email): @@ -117,7 +117,7 @@ def remove_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=self.VERIFY_REQUEST) + view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) return view_permission def user_has_single_permission(self, permission, value): @@ -126,7 +126,7 @@ def user_has_single_permission(self, permission, value): f.args["item"] = 'Hypatio.' + permission try: - user_permissions = requests.get(f.url, headers=self.JWT_HEADERS, verify=self.VERIFY_REQUEST).json() + user_permissions = requests.get(f.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() except JSONDecodeError: logger.debug("[SCIAUTHZ][user_has_single_permission] - No Valid permissions returned.") user_permissions = {"count": 0} diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 64c92be4..99e8c27a 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -141,6 +141,7 @@ COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN") SSL_SETTING = "https" +VERIFY_REQUESTS = True CONTACT_FORM_RECIPIENTS="dbmi_tech_core@hms.harvard.edu" DEFAULT_FROM_EMAIL="dbmi_tech_core@hms.harvard.edu" diff --git a/app/profile/views.py b/app/profile/views.py index 912e7a59..8dfc6558 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -42,11 +42,11 @@ def update_profile(request): # Create a new registration with a POST if registration_form.cleaned_data['id'] == "": - requests.post(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=False) + requests.post(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) # Update an existing registration with a PUT to the specific ID else: registration_url = settings.SCIREG_REGISTRATION_URL + registration_form.cleaned_data['id'] + '/' - requests.put(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=False) + requests.put(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) return HttpResponse(200) else: @@ -67,7 +67,7 @@ def profile(request, template_name='profile/profile.html'): jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} # Query SciReg to get the user's information - registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() + registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() logger.debug('[HYPATIO][DEBUG] Registration info ' + json.dumps(registration_info)) diff --git a/app/projects/views.py b/app/projects/views.py index a7cb0e03..acc7aa40 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -431,7 +431,7 @@ def grant_access_with_view_permissions(request): # Grab the full authorization request object from SciAuthZ to have all fields necessary for serialization authorization_request_url = settings.AUTHORIZATION_REQUEST_URL + "?id=" + authorization_request_id - authorization_request = requests.get(authorization_request_url, headers=jwt_headers, verify=False).json() + authorization_request = requests.get(authorization_request_url, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() if authorization_request is not None and 'results' in authorization_request: authorization_request_data = authorization_request['results'][0] @@ -445,7 +445,7 @@ def grant_access_with_view_permissions(request): authorization_request_data['date_request_granted'] = current_time authorization_request_url = settings.AUTHORIZATION_REQUEST_GRANT_URL + authorization_request_id + '/' - requests.put(authorization_request_url, headers=jwt_headers, data=json.dumps(authorization_request_data), verify=False) + requests.put(authorization_request_url, headers=jwt_headers, data=json.dumps(authorization_request_data), verify=settings.VERIFY_REQUESTS) user_permission = {"user": person_email, "item": project, @@ -454,7 +454,7 @@ def grant_access_with_view_permissions(request): # Add a VIEW permission to the user permissions_url = settings.USER_PERMISSIONS_URL - user_permissions = requests.post(permissions_url, headers=jwt_headers, data=json.dumps(user_permission), verify=False) + user_permissions = requests.post(permissions_url, headers=jwt_headers, data=json.dumps(user_permission), verify=settings.VERIFY_REQUESTS) return HttpResponse(200) @@ -492,7 +492,7 @@ def project_details(request, project_key): get_task_context_data(request) # Make a request to SciReg to grab email verification and profile information - profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=False).json() + profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() if profile_registration_info['count'] != 0: profile_registration_info = profile_registration_info["results"][0] From ce52a74b34f52a27bb3c367fba0d5e01ad7f74d6 Mon Sep 17 00:00:00 2001 From: Michael Tommie McDuffie Date: Mon, 16 Apr 2018 07:52:59 -0400 Subject: [PATCH 187/613] TC-192 - Auth0 Algorithm update --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index 1e20b7b4..136d8adf 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,7 +11,7 @@ furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 Pillow==5.0.0 -py-auth0-jwt==0.2.5 +py-auth0-jwt==0.2.7 py-auth0-jwt-rest==0.1 python-pstore==0.8 pycrypto==2.6.1 From 5e286039391dfd607b184235617a677ade546c22 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 16 Apr 2018 10:44:12 -0400 Subject: [PATCH 188/613] TC-192 - Using proper validation function for given Argument. --- app/profile/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/profile/views.py b/app/profile/views.py index 8dfc6558..9dbe6487 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -7,7 +7,7 @@ from django.contrib import messages from django.conf import settings from django.shortcuts import render -from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_jwt, logout_redirect +from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_request as validate_jwt, logout_redirect from .forms import RegistrationForm from django.http import HttpResponse From 3f79e54feec6b2eddfef31dc0f5d73c524acafc9 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 16 Apr 2018 10:54:21 -0400 Subject: [PATCH 189/613] TC-192 - Updating libraries.. --- app/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/requirements.txt b/app/requirements.txt index 136d8adf..49ac452a 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -14,8 +14,7 @@ Pillow==5.0.0 py-auth0-jwt==0.2.7 py-auth0-jwt-rest==0.1 python-pstore==0.8 -pycrypto==2.6.1 -PyJWT==1.4.2 +PyJWT==1.6.1 PyMySQL==0.7.9 raven==6.1.0 requests==2.11.1 From fd5d5546b4af0dc4b7daaf7c18759f590c0b3413 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 16 Apr 2018 11:08:17 -0400 Subject: [PATCH 190/613] TC-192 - Missed a file. --- app/projects/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/projects/views.py b/app/projects/views.py index acc7aa40..76dc440a 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -14,7 +14,7 @@ from pyauth0jwt.auth0authenticate import user_auth_and_jwt from pyauth0jwt.auth0authenticate import public_user_auth_and_jwt -from pyauth0jwt.auth0authenticate import validate_jwt +from pyauth0jwt.auth0authenticate import validate_request as validate_jwt from pyauth0jwt.auth0authenticate import logout_redirect from .models import DataProject From 7be23b00549c265eeb7e77aecbea157f5670d41c Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 16 Apr 2018 11:30:01 -0400 Subject: [PATCH 191/613] TC-192 - Testing updated package... --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index 49ac452a..8a4c8281 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,7 +11,7 @@ furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 Pillow==5.0.0 -py-auth0-jwt==0.2.7 +py-auth0-jwt==0.2.8 py-auth0-jwt-rest==0.1 python-pstore==0.8 PyJWT==1.6.1 From f00065936fb740df0749357113b8236c395ac7ba Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 17 Apr 2018 16:17:00 -0400 Subject: [PATCH 192/613] TC-186: Permission checks and download capability Checks if a user has the proper permissions to a project before uploading their submission. Increases the expires value for S3 uploads to support clients with a small time difference compared to the server. Separates S3 file storage for project files from participant submissions into different buckets. Adds a table to the manage team screen for admins to be able to view participant submissions. Adds functionality to download those participant submissions if the user is a manager of the project. --- app/hypatio/file_services.py | 12 ++++ app/hypatio/settings.py | 2 +- app/projects/admin.py | 5 ++ .../migrations/0031_auto_20180409_1353.py | 2 +- .../0032_participantsubmissiondownload.py | 27 +++++++++ app/projects/models.py | 24 +++++++- app/projects/urls.py | 7 ++- app/projects/views.py | 10 +++- app/projects/views_files.py | 55 ++++++++++++++++--- app/templates/datacontests/manageteams.html | 33 +++++++++++ app/templates/project_participate.html | 6 +- 11 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 app/projects/migrations/0032_participantsubmissiondownload.py diff --git a/app/hypatio/file_services.py b/app/hypatio/file_services.py index 2a931efb..7556e835 100644 --- a/app/hypatio/file_services.py +++ b/app/hypatio/file_services.py @@ -217,6 +217,7 @@ def create_file(request, filename, metadata, tags=[]): params = { 'cloud': 'aws', 'bucket': settings.FILESERVICE_AWS_BUCKET, + 'expires': 100, } # Make the request for an s3 presigned post. @@ -239,6 +240,9 @@ def uploaded_file(request, uuid, location_id): def download_file(request, uuid): + """ + Returns a download URL generated by FileService. + """ # Make the request. response = get(request, '/api/file/{}/download/'.format(uuid)) @@ -259,6 +263,10 @@ def _s3_client(): def get_download_url(file_name, expires_in=3600): + """ + Returns an S3 URL for project related files not tracked by fileservice. + """ + logger.debug('[file_services][get_download_url] Generating URL for {}'.format(file_name)) # Generate the URL to get the file object @@ -277,6 +285,10 @@ def get_download_url(file_name, expires_in=3600): def upload_file(file_name, file, expires_in=3600): + """ + Enables uploading of files directly to the Hypatio S3 bucket without fileservice tracking. + """ + logger.error('[file_services][upload_file] Uploading file: {}'.format(file_name)) # Generate the POST attributes diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index d1645fc3..9b3e95d2 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -225,7 +225,7 @@ FILESERVICE_API_URL = os.environ.get('FILESERVICE_API_URL') FILESERVICE_GROUP = os.environ.get('FILESERVICE_GROUP') -FILESERVICE_AWS_BUCKET = os.environ.get('S3_BUCKET') +FILESERVICE_AWS_BUCKET = os.environ.get('FILESERVICE_AWS_BUCKET') FILESERVICE_SERVICE_ACCOUNT = 'hypatio' FILESERVICE_SERVICE_TOKEN = os.environ.get('FILESERVICE_SERVICE_TOKEN') diff --git a/app/projects/admin.py b/app/projects/admin.py index 6b257c81..4516cd20 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -9,6 +9,7 @@ from .models import HostedFile from .models import HostedFileDownload from .models import ParticipantSubmission +from .models import ParticipantSubmissionDownload class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') #, 'project_url') @@ -40,6 +41,9 @@ class HostedFileDownloadAdmin(admin.ModelAdmin): class ParticipantSubmissionAdmin(admin.ModelAdmin): list_display = ('participant', 'upload_date', 'uuid', 'location') +class ParticipantSubmissionDownloadAdmin(admin.ModelAdmin): + list_display = ('user', 'participant_submission', 'download_date') + admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) admin.site.register(SignedAgreementForm, SignedagreementformAdmin) @@ -50,3 +54,4 @@ class ParticipantSubmissionAdmin(admin.ModelAdmin): admin.site.register(HostedFile, HostedFileAdmin) admin.site.register(HostedFileDownload, HostedFileDownloadAdmin) admin.site.register(ParticipantSubmission, ParticipantSubmissionAdmin) +admin.site.register(ParticipantSubmissionDownload, ParticipantSubmissionDownloadAdmin) diff --git a/app/projects/migrations/0031_auto_20180409_1353.py b/app/projects/migrations/0031_auto_20180409_1353.py index 48a9bf80..b4699ea5 100644 --- a/app/projects/migrations/0031_auto_20180409_1353.py +++ b/app/projects/migrations/0031_auto_20180409_1353.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='participantsubmission', name='location', - field=models.CharField(default=None, max_length=12), + field=models.CharField(default=None, max_length=12, blank=True, null=True), ), migrations.AddField( model_name='participantsubmission', diff --git a/app/projects/migrations/0032_participantsubmissiondownload.py b/app/projects/migrations/0032_participantsubmissiondownload.py new file mode 100644 index 00000000..2dde94e0 --- /dev/null +++ b/app/projects/migrations/0032_participantsubmissiondownload.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-17 19:33 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0031_auto_20180409_1353'), + ] + + operations = [ + migrations.CreateModel( + name='ParticipantSubmissionDownload', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('download_date', models.DateTimeField(auto_now_add=True)), + ('participant_submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.ParticipantSubmission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index eae4aa0b..5f09ca5e 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -177,6 +177,10 @@ def __str__(self): class HostedFile(models.Model): + """ + Tracks the files belonging to projects that users will be able to download. + """ + long_name = models.CharField(max_length=100, blank=False, null=False) description = models.CharField(max_length=2000, blank=True, null=True) file_name = models.CharField(max_length=100, blank=False, null=False) @@ -190,6 +194,10 @@ def __str__(self): class HostedFileDownload(models.Model): + """ + Tracks who is attempting to download a hosted file. + """ + user = models.ForeignKey(User) hosted_file = models.ForeignKey(HostedFile) download_date = models.DateTimeField(auto_now_add=True) @@ -205,13 +213,23 @@ def __str__(self): return '%s %s %s' % (self.user, self.team, self.date) class ParticipantSubmission(models.Model): - """Captures the files that participants are submitting for their challenges. Through the Participant model + """ + Captures the files that participants are submitting for their challenges. Through the Participant model you can get to what team and project this submission pertains to. """ participant = models.ForeignKey(Participant) upload_date = models.DateTimeField(auto_now_add=True) uuid = models.UUIDField(null=False, unique=True, primary_key=True, default=None) - location = models.CharField(max_length=12, default=None) + location = models.CharField(max_length=12, default=None, blank=True, null=True) def __str__(self): - return '%s %s' % (self.participant.user, self.file) + return '%s' % (self.uuid) + +class ParticipantSubmissionDownload(models.Model): + """ + Tracks who is attempting to download a participant's submission. + """ + + user = models.ForeignKey(User) + participant_submission = models.ForeignKey(ParticipantSubmission, on_delete=models.CASCADE) + download_date = models.DateTimeField(auto_now_add=True) diff --git a/app/projects/urls.py b/app/projects/urls.py index a0034fd9..cbca727f 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -23,8 +23,10 @@ from .views_teams import save_team_comment from .views_teams import change_signed_form_status from .views_teams import download_signed_form + from .views_files import download_dataset -from .views_files import upload_dataset +from .views_files import upload_participantsubmission_file +from .views_files import download_participantsubmission_file urlpatterns = ( url(r'^$', list_data_projects), @@ -50,6 +52,7 @@ url(r'^team_signup_form/(P[^/]+)/$', team_signup_form), url(r'^signed_agreement_form/$', signed_agreement_form), url(r'^download_dataset/$', download_dataset), - url(r'^upload_dataset/$', upload_dataset), + url(r'^upload_participantsubmission_file/$', upload_participantsubmission_file), + url(r'^download_participantsubmission_file/$', download_participantsubmission_file), url(r'^(?P[^/]+)/$', project_details), ) diff --git a/app/projects/views.py b/app/projects/views.py index a7cb0e03..5c4418d6 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -25,6 +25,7 @@ from .models import HostedFile from .models import HostedFileDownload from .models import TeamComment +from .models import ParticipantSubmission from profile.views import get_task_context_data from profile.forms import RegistrationForm @@ -316,10 +317,11 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m # Get the comments made about this team by challenge administrators comments = TeamComment.objects.filter(team=team) - # Get a history of files downloaded for members of this team + # Get a history of files downloaded and uploaded by members of this team files = HostedFile.objects.filter(project=project) team_users = User.objects.filter(participant__in=team_participants) downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) + uploads = ParticipantSubmission.objects.filter(participant__in=team_participants) return render(request, template_name, context={"user": user, "ssl_setting": settings.SSL_SETTING, @@ -331,7 +333,8 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m "team_has_all_forms_complete": team_has_all_forms_complete, "institution": institution, "comments": comments, - "downloads": downloads}) + "downloads": downloads, + "uploads": uploads}) @user_auth_and_jwt def manage_contest(request, project_key, template_name='datacontests/managecontests.html'): @@ -543,9 +546,10 @@ def project_details(request, project_key): 'already_signed': already_signed}) try: + # Only allow a user onto the project participation page if they are on an Active team and they have VIEW permissions participant = Participant.objects.get(user=user) team = participant.team - access_granted = participant.team_approved and team.status == 'Active' + access_granted = participant.team_approved and team.status == 'Active' and sciauthz.user_has_single_permission("n2c2-t1", "VIEW") except ObjectDoesNotExist: participant = None team = None diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 5d5d94d6..5172183a 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -16,6 +16,7 @@ from .models import HostedFile from .models import HostedFileDownload from .models import ParticipantSubmission +from .models import ParticipantSubmissionDownload from .models import Participant logger = logging.getLogger(__name__) @@ -23,6 +24,11 @@ @user_auth_and_jwt def download_dataset(request): + """ + Handles downloads for project level files. Checks that the requesting user + has view permissions on the given project before allowing a download. + """ + logger.debug("[views_files][download_dataset] - Attempting file download.") # Check Permissions in SciAuthZ @@ -31,7 +37,7 @@ def download_dataset(request): if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): logger.debug("[views_files][download_dataset] - No Access for user " + request.user.email) - return HttpResponse("403 Forbidden. You do not have access to download this file.") + return HttpResponse("You do not have access to download this file.", status=403) file_id = request.GET.get("file_id") file_to_download = get_object_or_404(HostedFile, id=file_id) @@ -50,17 +56,53 @@ def download_dataset(request): return response +@user_auth_and_jwt +def download_participantsubmission_file(request): + """ + Handles downloads of participant submission files. Checks that the requesting user + has proper permissions to access this file. + """ + logger.debug('download_participantsubmission_file: {}'.format(request.method)) + + if request.method == "GET": + + project_key = request.GET.get("project_key", "") + fileservice_uuid = request.GET.get("uuid", "") + + # Check Permissions in SciAuthZ + user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + is_manager = sciauthz.user_has_manage_permission(request, project_key) + + if not is_manager: + logger.debug("[views_files][download_participantsubmission_file] - No Access for user " + request.user.email) + return HttpResponse("You do not have access to download this file.", status=403) + + # Save a record of this person downloading this file + participant_submission = ParticipantSubmission.objects.get(uuid=fileservice_uuid) + ParticipantSubmissionDownload.objects.create(user=request.user, participant_submission=participant_submission) + + url = fileservice.download_file(request, fileservice_uuid) + + response = redirect(url) + response['Content-Disposition'] = 'attachment' + + return response @user_auth_and_jwt -def upload_dataset(request): - logger.debug('upload_dataset: {}'.format(request.method)) +def upload_participantsubmission_file(request): + logger.debug('upload_participantsubmission_file: {}'.format(request.method)) - # Check method if request.method == 'POST': logger.debug('post') - # Check user permissions - # TODO: Finish this + # Check Permissions in SciAuthZ + user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + + if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): + logger.debug("[views_files][upload_participantsubmission_file] - No Access for user " + request.user.email) + return HttpResponse("You do not have access to upload this file.", status=403) # Assembles the form and run validation. filename = request.POST.get('filename') @@ -70,7 +112,6 @@ def upload_dataset(request): return HttpResponse('Filename and project are required', status=400) # Prepare the metadata - # TODO: Finish this metadata = { 'project': project, 'uploader': request.user.email, diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index a35dd381..c60bec58 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -137,6 +137,32 @@

    Team members

    +
    +
    +

    Data Uploads

    +
    +
    + + + + + + + + + + {% for upload in uploads %} + + + + + + {% endfor %} + +
    PersonDateFile
    {{ upload.participant.user.email }}{{ upload.upload_date|timezone:"America/New_York" }} (EST)Download
    +
    +
    +

    Data downloads

    @@ -202,6 +228,13 @@

    Previous comments:

    "order": [[3, "desc"]] // Sort by status column (4th column) }); + $('#team-uploads-table').DataTable({ + "paging": false, + "info": false, + "searching": false, + "order": [[2, "desc"]] // Sort by date column (3rd column) + }); + $('#team-downloads-table').DataTable({ "paging": false, "info": false, diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index ff914913..d114056e 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -211,7 +211,7 @@
    Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Date: Fri, 20 Apr 2018 13:47:31 -0400 Subject: [PATCH 193/613] TC-202: Adds submission form and stores answer in ParticipantSubmission object Projects can now have submission forms that are dynamically pulled from an html file and displayed for participants to fill out when uploading their file. The response to the form is serialized into a json and then stored in the ParticipantSubmission object. This commit also fixes a small bug with new users where profile_registration_info['count'] is not an available key from scireg. --- .../6b40a9a6-5190-4559-90b3-1f19d380a33c.html | 51 +++++++ app/profile/views.py | 8 +- app/projects/fixtures/projects.json | 3 +- .../0033_dataproject_submission_form_html.py | 23 ++++ .../migrations/0034_auto_20180419_1704.py | 22 +++ ...ipantsubmission_submission_form_answers.py | 20 +++ .../migrations/0036_auto_20180420_1458.py | 24 ++++ app/projects/models.py | 14 +- app/projects/templatetags/projects_extras.py | 4 +- app/projects/views.py | 2 +- app/projects/views_files.py | 46 ++++--- app/templates/project_participate.html | 126 +++++++++--------- app/templates/project_signup.html | 2 +- 13 files changed, 250 insertions(+), 95 deletions(-) create mode 100644 app/media/submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html create mode 100644 app/projects/migrations/0033_dataproject_submission_form_html.py create mode 100644 app/projects/migrations/0034_auto_20180419_1704.py create mode 100644 app/projects/migrations/0035_participantsubmission_submission_form_answers.py create mode 100644 app/projects/migrations/0036_auto_20180420_1458.py diff --git a/app/media/submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html b/app/media/submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html new file mode 100644 index 00000000..1ec171f3 --- /dev/null +++ b/app/media/submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html @@ -0,0 +1,51 @@ +
    +
    + + +
    + +
    + + +
    + + +
    +
    + +
    + + +
    + + + +
    +
    + + + +
    + + +
    +
    \ No newline at end of file diff --git a/app/profile/views.py b/app/profile/views.py index 9dbe6487..52fc1ae6 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -2,19 +2,19 @@ import logging import requests - +from pyauth0jwt.auth0authenticate import user_auth_and_jwt +from pyauth0jwt.auth0authenticate import validate_request as validate_jwt +from pyauth0jwt.auth0authenticate import logout_redirect from django.contrib import messages from django.conf import settings from django.shortcuts import render -from pyauth0jwt.auth0authenticate import user_auth_and_jwt, validate_request as validate_jwt, logout_redirect -from .forms import RegistrationForm from django.http import HttpResponse from hypatio.sciauthz_services import SciAuthZ - from hypatio import scireg_services +from .forms import RegistrationForm # Get an instance of a logger logger = logging.getLogger(__name__) diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index dceb7bf4..ae2d4618 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -99,7 +99,8 @@ "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, - "visible": true + "visible": true, + "submission_form_html": "submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html" } }, { diff --git a/app/projects/migrations/0033_dataproject_submission_form_html.py b/app/projects/migrations/0033_dataproject_submission_form_html.py new file mode 100644 index 00000000..4c5fd28b --- /dev/null +++ b/app/projects/migrations/0033_dataproject_submission_form_html.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-19 17:03 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import projects.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0032_participantsubmissiondownload'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='submission_form_html', + field=models.FileField(default=None, upload_to=projects.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + preserve_default=False, + ), + ] diff --git a/app/projects/migrations/0034_auto_20180419_1704.py b/app/projects/migrations/0034_auto_20180419_1704.py new file mode 100644 index 00000000..ddde3118 --- /dev/null +++ b/app/projects/migrations/0034_auto_20180419_1704.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-19 17:04 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import projects.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0033_dataproject_submission_form_html'), + ] + + operations = [ + migrations.AlterField( + model_name='dataproject', + name='submission_form_html', + field=models.FileField(blank=True, null=True, upload_to=projects.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + ), + ] diff --git a/app/projects/migrations/0035_participantsubmission_submission_form_answers.py b/app/projects/migrations/0035_participantsubmission_submission_form_answers.py new file mode 100644 index 00000000..b78715a5 --- /dev/null +++ b/app/projects/migrations/0035_participantsubmission_submission_form_answers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-20 13:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0034_auto_20180419_1704'), + ] + + operations = [ + migrations.AddField( + model_name='participantsubmission', + name='submission_form_answers', + field=models.CharField(blank=True, default=None, max_length=2500, null=True), + ), + ] diff --git a/app/projects/migrations/0036_auto_20180420_1458.py b/app/projects/migrations/0036_auto_20180420_1458.py new file mode 100644 index 00000000..24f35a05 --- /dev/null +++ b/app/projects/migrations/0036_auto_20180420_1458.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-20 14:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0035_participantsubmission_submission_form_answers'), + ] + + operations = [ + migrations.RemoveField( + model_name='participantsubmission', + name='submission_form_answers', + ), + migrations.AddField( + model_name='participantsubmission', + name='submission_info', + field=models.TextField(blank=True, default=None, null=True), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 5f09ca5e..006d40f2 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -36,6 +36,14 @@ def get_agreement_form_upload_path(instance, filename): return '%s/%s.%s' % (form_directory, file_name, file_extension) +def get_submission_form_upload_path(instance, filename): + + form_directory = 'submissionforms/' + file_name = uuid.uuid4() + file_extension = filename.split('.')[-1] + return '%s/%s.%s' % (form_directory, file_name, file_extension) + + def get_institution_logo_upload_path(instance, filename): form_directory = 'institutionlogos/' @@ -89,6 +97,7 @@ class DataProject(models.Model): visible = models.BooleanField(default=False, blank=False, null=False) registration_open = models.BooleanField(default=False, blank=False, null=False) accepting_user_submissions = models.BooleanField(default=False, blank=False, null=False) + submission_form_html = models.FileField(upload_to=get_submission_form_upload_path, validators=[FileExtensionValidator(allowed_extensions=['html'])], blank=True, null=True) def __str__(self): return '%s' % (self.project_key) @@ -215,12 +224,15 @@ def __str__(self): class ParticipantSubmission(models.Model): """ Captures the files that participants are submitting for their challenges. Through the Participant model - you can get to what team and project this submission pertains to. + you can get to what team and project this submission pertains to. The location field is for fileservice + integration. The submission_form_answers field stores any answers a participant might provide when + submitting their work. """ participant = models.ForeignKey(Participant) upload_date = models.DateTimeField(auto_now_add=True) uuid = models.UUIDField(null=False, unique=True, primary_key=True, default=None) location = models.CharField(max_length=12, default=None, blank=True, null=True) + submission_info = models.TextField(default=None, blank=True, null=True) def __str__(self): return '%s' % (self.uuid) diff --git a/app/projects/templatetags/projects_extras.py b/app/projects/templatetags/projects_extras.py index 1abd6de3..b0860d0a 100644 --- a/app/projects/templatetags/projects_extras.py +++ b/app/projects/templatetags/projects_extras.py @@ -9,9 +9,9 @@ register = template.Library() @register.filter -def get_agreement_form_file_contents(agreement_form_file_name): +def get_html_form_file_contents(form_file_name): - form_path = os.path.join(settings.MEDIA_ROOT, agreement_form_file_name) + form_path = os.path.join(settings.MEDIA_ROOT, form_file_name) return open(form_path, 'r').read() @register.filter diff --git a/app/projects/views.py b/app/projects/views.py index 2d890841..2070537c 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -497,7 +497,7 @@ def project_details(request, project_key): # Make a request to SciReg to grab email verification and profile information profile_registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() - if profile_registration_info['count'] != 0: + if profile_registration_info.get('count', 0) != 0: profile_registration_info = profile_registration_info["results"][0] email_verified = profile_registration_info['email_confirmed'] diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 5172183a..952b580e 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -1,4 +1,6 @@ import logging +import json +from copy import copy from django.shortcuts import get_object_or_404 from django.conf import settings @@ -91,12 +93,17 @@ def download_participantsubmission_file(request): @user_auth_and_jwt def upload_participantsubmission_file(request): + """ + On a POST, send metadata about the user's file to fileservice to get back an S3 upload link. + On a PATCH, check to see that the file successfully was uploaded to S3 and then create a new + ParticipantSubmission record. + """ logger.debug('upload_participantsubmission_file: {}'.format(request.method)) if request.method == 'POST': logger.debug('post') - # Check Permissions in SciAuthZ + # Check that user has permissions to be submitting files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) @@ -104,14 +111,14 @@ def upload_participantsubmission_file(request): logger.debug("[views_files][upload_participantsubmission_file] - No Access for user " + request.user.email) return HttpResponse("You do not have access to upload this file.", status=403) - # Assembles the form and run validation. + # Assembles the form and runs validation. filename = request.POST.get('filename') project = request.POST.get('project') if not filename or not project: logger.error('No filename or no project!') return HttpResponse('Filename and project are required', status=400) - # Prepare the metadata + # Prepare the metadata. metadata = { 'project': project, 'uploader': request.user.email, @@ -119,24 +126,19 @@ def upload_participantsubmission_file(request): 'app': 'hypatio', } - # Get the file link + # Create a new record in fileservice for this file and get back information on where it should live in S3. uuid, response = fileservice.create_file(request, filename, metadata) - - # Get the needed bits. post = response['post'] location = response['locationid'] # Form the data for the File object. file = {'uuid': uuid, 'location': location, 'filename': filename} - logger.debug('File: {}'.format(file)) - # Build the response response = { 'post': post, 'file': file, } - logger.debug('Response: {}'.format(post)) return JsonResponse(data=response) @@ -144,20 +146,29 @@ def upload_participantsubmission_file(request): elif request.method == 'PATCH': logger.debug('patch') - # Get the data + # Get the data. data = QueryDict(request.body) - logger.debug('Data: {}'.format(data)) try: - # Get the participant + # Get the participant. participant = Participant.objects.get(user=request.user) - # Create the object and save UUID and location for future downloads + # Prepare a json that holds information about the file and the original submission form. + # This is used later as included metadata when downloading the participant's submission. + # Remove a few unnecessary fields. + submission_info = copy(data) + del submission_info['csrfmiddlewaretoken'] + del submission_info['location'] + submission_info_json = json.dumps(submission_info) + + # Create the object and save UUID and location for future downloads. ParticipantSubmission.objects.create( participant=participant, uuid=data['uuid'], - location=data['location']) + location=data['location'], + submission_info=submission_info_json + ) # Make the request to FileService. if not fileservice.uploaded_file(request, data['uuid'], data['location']): @@ -169,16 +180,9 @@ def upload_participantsubmission_file(request): except exceptions.ObjectDoesNotExist as e: logger.exception(e) - return HttpResponse(status=404) - except Exception as e: logger.exception(e) - return HttpResponse(status=500) else: - return HttpResponse("Invalid method", status=403) - - - diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index d114056e..40dbc277 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -119,7 +119,7 @@

    {{ file.long_name }}

    {% if project.accepting_user_submissions %}
    {% if team.get_number_of_submissions_left == 0 %} @@ -128,21 +128,35 @@

    Uploads

    -
    Your team has {{ team.get_number_of_submissions_left }} submissions left.
    -
    - {% csrf_token %} - - - - +
    Your team has {{ team.get_number_of_submissions_left }} submissions left.
    + +
    + + + {% if project.submission_form_html %} +
    + {{ project.submission_form_html.name|get_html_form_file_contents | safe }} +
    + {% endif %} + +
    + +
    +
    + + + +
    + + {% csrf_token %}
    - {% endif %} + {% endif %}
    {% endif %} @@ -167,11 +181,10 @@
    Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. Your team has {{ team.get_number_of_submissions_left }} submissions left. {% if not agreement_form.already_signed %}
    - {{ agreement_form.agreement_form_file|get_agreement_form_file_contents | safe }} + {{ agreement_form.agreement_form_file|get_html_form_file_contents | safe }}
    From 26da83b43eb642ac0b2cd46c4d271710d2abb8b1 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 23 Apr 2018 12:34:29 -0400 Subject: [PATCH 194/613] TC-201: Enforce zip files as only file upload extension accepted --- app/hypatio/sciauthz_services.py | 2 +- app/projects/fixtures/projects.json | 7 +++--- app/projects/views_files.py | 4 ++++ app/templates/project_participate.html | 31 ++++++++++++-------------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index 82173f8f..39122d9e 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -117,7 +117,7 @@ def remove_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) + view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) return view_permission def user_has_single_permission(self, permission, value): diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index ae2d4618..87953885 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -40,7 +40,9 @@ "agreement_forms_required": true, "agreement_forms": [1, 2], "project_supervisor": "nathaniel_bessa@hms.harvard.edu", - "is_contest": true + "is_contest": true, + "visible": true, + "submission_form_html": "submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html" } }, { @@ -99,8 +101,7 @@ "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, - "visible": true, - "submission_form_html": "submissionforms/6b40a9a6-5190-4559-90b3-1f19d380a33c.html" + "visible": true } }, { diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 952b580e..9e75cb45 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -118,6 +118,10 @@ def upload_participantsubmission_file(request): logger.error('No filename or no project!') return HttpResponse('Filename and project are required', status=400) + if filename.split(".")[1] != "zip": + logger.error('Not a zip file.') + return HttpResponse("Only .zip files are accepted", status=400) + # Prepare the metadata. metadata = { 'project': project, diff --git a/app/templates/project_participate.html b/app/templates/project_participate.html index 40dbc277..7c883d02 100644 --- a/app/templates/project_participate.html +++ b/app/templates/project_participate.html @@ -125,14 +125,10 @@

    Contest Submission

    {% if team.get_number_of_submissions_left == 0 %} Your team has used up all of its available submissions. {% else %} -

    {{ participant.user.email }} {% if participant.team_approved %} Approved {% elif participant.team_pending %} - - {% else %} @@ -54,28 +54,28 @@
    - {% if team.status == "Pending" and not team_has_pending_members %} - {% endif %} {% else %} - {% if participant.team_wait_on_leader %} -

    No action needed at this time. We are waiting for your team leader {{ participant.team_wait_on_leader_email }} to create a team and approve your access.

    + {% if step.participant.team_wait_on_leader %} +

    No action needed at this time. We are waiting for your team leader {{ step.participant.team_wait_on_leader_email }} to create a team and approve your access.


    Is your team leader using a different email address? Click the button below to return to the team join step.

    {% csrf_token %} - +
    - {% elif participant.team_pending %} - No action needed at this time. We are waiting for your team leader {{ team.team_leader }} to approve your request to join the team. - {% elif participant.team_approved %} + {% elif step.participant.team_pending %} + No action needed at this time. We are waiting for your team leader {{ step.team.team_leader }} to approve your request to join the team. + {% elif step.participant.team_approved %} {% else %} - + diff --git a/app/templates/project_signup_old.html b/app/templates/project_signup_old.html deleted file mode 100644 index 43cd5fb5..00000000 --- a/app/templates/project_signup_old.html +++ /dev/null @@ -1,227 +0,0 @@ -{% extends 'base.html' %} -{% load projects_extras %} -{% load bootstrap3 %} - -{% block headscripts %} -{% endblock %} - -{% block tab_name %}{{ project.name }}{% endblock %} -{% block title %}{{ project.name }}{% endblock %} -{% block subtitle %}{{ project.short_description }}{% endblock %} - -{% block content %} - -{% if messages %} - {% include 'messages.html' %} -{% endif %} - -
    - -
    -
    - -
    - {% autoescape off %} - {{ project.description }} - {% endautoescape %} -
    -
    -
    - -
    - - {% if project.registration_open or user.is_superuser %} - {% if project.is_contest %} - - {% else %} - - {% endif %} - -
    - - -
    - - {% if current_step == 'verify_email' %} -
    - Your primary email on record needs to be verified. Please look in your inbox for the verification email or click below to send another one: - {% include 'profile/verify_email.html' %} -
    - {% endif %} -
    - - -
    - - {% if current_step == 'complete_profile' %} -
    -

    There are a few required fields in your profile that need to be filled out. Please complete the profile registration form below, specifically the fields marked as required. You can go back and edit these fields any time by visiting your profile page.

    - - {% include 'profile/registration_form.html' %} -
    - {% endif %} -
    - - - {% for agreement_form in agreement_forms_list %} -
    - - {% if current_step == agreement_form.agreement_form_name %} -
    - {% if not agreement_form.already_signed %} -
    -
    - {{ agreement_form.agreement_form_path|get_html_form_file_contents | safe }} -
    - - - - - -
    - -
    - -
    - - {% csrf_token %} -
    - {% endif %} -
    - {% endif %} -
    - {% endfor %} - - - {% if project.is_contest %} -
    - - {% if current_step == 'team' %} -
    - {% include "datacontests/teamsetup.html" %} -
    - {% endif %} -
    - {% endif %} - - - {% if project.show_jwt %} - {% include "shared/jwt_dialog.html" %} - {% endif %} - - - {% if project.permission_scheme == "PRIVATE" and not agreement_forms_list %} - {% include "shared/request_access.html" %} - {% endif %} - -
    - {% else %} - - {% endif %} - -
    -
    -{% endblock %} - -{% block footerscripts %} - - - -{% endblock %} \ No newline at end of file diff --git a/app/templates/shared/jwt_dialog.html b/app/templates/shared/jwt_dialog.html deleted file mode 100644 index 460fc4f1..00000000 --- a/app/templates/shared/jwt_dialog.html +++ /dev/null @@ -1,18 +0,0 @@ -
    - - {% if current_step == 'jwt' %} -
    - Below is your JWT which you can use to authenticate yourself with DBMI APIs. Proceed with caution! Treat this - long string as you would a password! It represents your identity and anyone who has it can access the same - resources as you. Refer to your project's instructions for specifics on how to use this token. -

    - -
    - {% endif %} -
    \ No newline at end of file diff --git a/app/templates/project_login_or_register.html b/app/templates/shared/login_or_register.html similarity index 100% rename from app/templates/project_login_or_register.html rename to app/templates/shared/login_or_register.html diff --git a/app/templates/shared/request_access.html b/app/templates/shared/request_access.html deleted file mode 100644 index 6d14e2f5..00000000 --- a/app/templates/shared/request_access.html +++ /dev/null @@ -1,53 +0,0 @@ -{% load static %} - -
    - - {% if current_step == 'request_access' %} -
    - {% if user_access_request %} - Your request is being reviewed. Please use the 'Contact Us' link for any further questions. - {% else %} -
    - - In order to gain access to this data an administrator will have to contact you with further steps. Click below to confirm your request.

    - - - - - - -
    - - {% csrf_token %} -
    - {% endif %} -
    - {% endif %} -
    - - diff --git a/app/templates/signed_agreement_form.html b/app/templates/shared/signed_agreement_form.html similarity index 100% rename from app/templates/signed_agreement_form.html rename to app/templates/shared/signed_agreement_form.html From 4b877b8e991b4a9c5c4f75c4b4f9975a4006e636 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 11 May 2018 14:32:10 -0400 Subject: [PATCH 220/613] TC-215: Display total submissions and total teams with submissions on contest management screen --- app/projects/views.py | 31 +++++++------------ .../datacontests/managecontests.html | 4 --- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/app/projects/views.py b/app/projects/views.py index d70ebc10..67bc10f7 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -369,16 +369,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte for person in users_without_a_team: email = person.email - - # TODO: commented out because these are too many api calls to make - # Make a request to SciReg for a specific person's user information - # user_info_json = get_user_profile(user_jwt, email, project_key) - - # if user_info_json['count'] != 0: - # user_info = user_info_json["results"][0] - # else: - # user_info = None - + signed_agreement_forms = [] # For each of the available agreement forms for this project, display only latest version completed by the user @@ -393,11 +384,12 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte }) # Simple statistics for display - total_teams = teams.count() - total_participants = Participant.objects.filter(data_challenge=project).count() - countries_represented = '?' # TODO - total_submissions = 0 # TODO - teams_with_any_submission = 0 # TODO + all_participants = Participant.objects.filter(data_challenge=project) + all_submissions = ParticipantSubmission.objects.filter( + participant__in=all_participants, + deleted=False + ) + teams_with_any_submission = all_submissions.select_related('participant').select_related('team') institution = project.institution @@ -407,11 +399,10 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte "project": project, "teams": teams, "users_without_a_team_details": users_without_a_team_details, - "total_teams": total_teams, - "total_participants": total_participants, - "countries_represented": countries_represented, - "total_submissions": total_submissions, - "teams_with_any_submission": teams_with_any_submission, + "total_teams": teams.count(), + "total_participants": all_participants.count(), + "total_submissions": all_submissions.count(), + "teams_with_any_submission": teams_with_any_submission.count(), "institution": institution}) @method_decorator(public_user_auth_and_jwt, name='dispatch') diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index b5163d11..490ffbf8 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -108,10 +108,6 @@

    Challenge Statistics

    {{ total_participants }} Total Participants -
  • - {{ countries_represented }} - Countries Represented -
  • {{ total_submissions }} Total Submissions From b22c94a0eab2dc1a43b00603e82ea36c340071d8 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 14 May 2018 09:33:01 -0400 Subject: [PATCH 221/613] TC-211: upgrades py-auth-jwt version to include email case sensitivity fix Previous version rejected jwt email comparison to django user email if casing did not match. --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index dc550d3e..8a0096de 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,7 +11,7 @@ furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 Pillow==5.0.0 -py-auth0-jwt==0.2.10 +py-auth0-jwt==0.2.12 py-auth0-jwt-rest==0.1 python-pstore==0.8 PyJWT==1.6.1 From 7cfd224377e90f6b4b0ee63fd4e26e7b11017889 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Wed, 16 May 2018 10:16:04 -0400 Subject: [PATCH 222/613] TC-190 - Adding dynamic form based signup. --- app/__init__.py | 0 app/projects/fixtures/projects.json | 32 ++++- app/projects/forms/__init__.py | 0 app/projects/forms/payerdb.py | 42 +++++++ app/projects/migrations/0001_initial.py | 2 +- .../migrations/0011_auto_20180206_1917.py | 2 +- .../migrations/0012_auto_20180206_2021.py | 2 +- .../migrations/0013_auto_20180208_2102.py | 2 +- .../migrations/0013_auto_20180209_1412.py | 2 +- .../0033_dataproject_submission_form_html.py | 2 +- .../migrations/0034_auto_20180419_1704.py | 2 +- .../migrations/0042_auto_20180515_1729.py | 60 ++++++++++ app/projects/models/__init__.py | 18 +++ app/projects/{ => models}/models.py | 18 +++ app/projects/models/payerdb.py | 40 +++++++ app/projects/steps/__init__.py | 0 app/projects/steps/dynamic_form.py | 112 ++++++++++++++++++ app/projects/tests.py | 44 +++++++ app/projects/urls.py | 2 + app/projects/views.py | 79 ++++++------ app/templates/dataprojects/__init__.py | 0 .../dynamic_agreement_form.html | 24 ++++ 22 files changed, 426 insertions(+), 59 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/projects/forms/__init__.py create mode 100644 app/projects/forms/payerdb.py create mode 100644 app/projects/migrations/0042_auto_20180515_1729.py create mode 100644 app/projects/models/__init__.py rename app/projects/{ => models}/models.py (95%) create mode 100644 app/projects/models/payerdb.py create mode 100644 app/projects/steps/__init__.py create mode 100644 app/projects/steps/dynamic_form.py create mode 100644 app/projects/tests.py create mode 100644 app/templates/dataprojects/__init__.py create mode 100644 app/templates/project_signup/dynamic_agreement_form.html diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index 92196720..78dca48d 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -27,6 +27,17 @@ "form_file_path": "agreementforms/n2c2-t1_roc.html" } }, + { + "model": "projects.agreementform", + "pk": 9, + "fields": { + "name": "Payer Database Access Request", + "short_name": "AREQ", + "created": "2018-01-01T00:00:00+00:00", + "form_file_path": "payerdb", + "type": "DJANGO" + } + }, { "model": "projects.dataproject", "pk": 6, @@ -37,7 +48,6 @@ "short_description": "2018 Track 1: Cohort Selection for Clinical Trials", "project_key": "n2c2-t1", "permission_scheme": "PRIVATE", - "agreement_forms_required": true, "agreement_forms": [1, 2], "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": true, @@ -55,7 +65,6 @@ "short_description": "Three Clinical Trials of Nutritional Supplements for Retinitis Pigmentosa", "project_key": "BERSON", "permission_scheme": "PRIVATE", - "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true @@ -98,7 +107,6 @@ "short_description": "A database of human exposomes and phenomes from the US National Health and Nutrition Examination Survey", "project_key": "NHANES", "permission_scheme": "PUBLIC", - "agreement_forms_required": false, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true @@ -114,7 +122,6 @@ "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository", "project_key": "GRDR", "permission_scheme": "PUBLIC", - "agreement_forms_required": true, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true @@ -130,7 +137,6 @@ "short_description": "Simons Simplex Collection", "project_key": "SSC", "permission_scheme": "PUBLIC", - "agreement_forms_required": true, "project_supervisor": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true @@ -146,7 +152,21 @@ "short_description": "Exposome Data Warehouse", "project_key": "EDW", "permission_scheme": "PUBLIC", - "agreement_forms_required": true, + "project_supervisor": "michael_mcduffie@hms.harvard.edu", + "is_contest": false, + "visible": true + } + }, + { + "model": "projects.dataproject", + "pk": 8, + "fields": { + "name": "Commercial Health Insurance Payer Database", + "institution": null, + "description": "

    What resources do you provide?

    • Payer database of a large commercial health insurance company
    • R packages to facilitate access to the database resources
    • R packages to implement standard workflows
    • Large scale analytic database / data mining platforms (SciDB, Apache Spark, etc.)

    How can I use these resources?

    Getting started is easy. You will be asked to fill out a Healthcare Data Science Program Access Request describing your team and what you’d like to do with the data, and we’ll contact you shortly. Applications are reviewed by program leadership on a monthly basis. Because we have many researchers in the HMS community who are working with us, we maintain an active roster of teams and their respective projects.

    If your project is approved, you will be required to sign a data use agreement that outlines the terms, conditions and purposes related to your access to the HDS resources.

    ", + "short_description": "Payer Database", + "project_key": "PAYERDB", + "permission_scheme": "PUBLIC", "project_supervisor": "michael_mcduffie@hms.harvard.edu", "is_contest": false, "visible": true diff --git a/app/projects/forms/__init__.py b/app/projects/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/projects/forms/payerdb.py b/app/projects/forms/payerdb.py new file mode 100644 index 00000000..c4516b58 --- /dev/null +++ b/app/projects/forms/payerdb.py @@ -0,0 +1,42 @@ +from django import forms +from ..models import PayerDBForm + + +class AccessRequestForm(forms.ModelForm): + + class Meta: + + model = PayerDBForm + exclude = ['user', 'agreement_form', 'project', 'date_signed', 'agreement_text', 'status'] + + @property + def render_title(self): + return """ +

    + Use this form to request access to the specified HDS resource(s) provided by the Department of Biomedical + Informatics at Harvard Medical School. Applications are currently only being accepted from HMS-affiliated + faculty members. Please understand that while we will make every effort to accommodate your request for + access, due to the large number of such requests and constrained resources, some applications may not be + approved, or will be entered into a queue for delayed access as resources become available. +

    + +

    + Approved requests for access will automatically expire after the study duration. Please re-submit this form + to request continuation of a prior project and provide a reference to your previous protocol in place of + a description of the proposed work (Section 2 below).
    +

    + +

    + Data access requests go through a three-stage approval process. First, the study team will be evaluated for + technical competency, second the science and related IRB approvals will be vetted by DBMI faculty, and + finally the request will be shared with the data owner (insurance company) for their approval and any + concomitant stipulations surrounding publication and intellectual property.
    +

    + +

    + Please address the following as completely as possible, and feel free to include any additional information + that you believe may help us to evaluate your request.
    +

    + +
    + """ \ No newline at end of file diff --git a/app/projects/migrations/0001_initial.py b/app/projects/migrations/0001_initial.py index 810e9958..067ab9f9 100644 --- a/app/projects/migrations/0001_initial.py +++ b/app/projects/migrations/0001_initial.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='name')), ('created', models.DateTimeField(auto_now_add=True)), - ('form_html', models.FileField(upload_to=projects.models.get_agreement_form_upload_path)), + ('form_html', models.FileField(upload_to=projects.models.models.get_agreement_form_upload_path)), ], ), migrations.CreateModel( diff --git a/app/projects/migrations/0011_auto_20180206_1917.py b/app/projects/migrations/0011_auto_20180206_1917.py index a91a3437..91723246 100644 --- a/app/projects/migrations/0011_auto_20180206_1917.py +++ b/app/projects/migrations/0011_auto_20180206_1917.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='name')), - ('logo', models.FileField(blank=True, null=True, upload_to=projects.models.get_institution_logo_upload_path)), + ('logo', models.FileField(blank=True, null=True, upload_to=projects.models.models.get_institution_logo_upload_path)), ], ), migrations.AlterField( diff --git a/app/projects/migrations/0012_auto_20180206_2021.py b/app/projects/migrations/0012_auto_20180206_2021.py index bde6c4d2..25ae3150 100644 --- a/app/projects/migrations/0012_auto_20180206_2021.py +++ b/app/projects/migrations/0012_auto_20180206_2021.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='institution', name='logo', - field=models.ImageField(blank=True, null=True, upload_to=projects.models.get_institution_logo_upload_path), + field=models.ImageField(blank=True, null=True, upload_to=projects.models.models.get_institution_logo_upload_path), ), ] diff --git a/app/projects/migrations/0013_auto_20180208_2102.py b/app/projects/migrations/0013_auto_20180208_2102.py index 5f5ceca0..8c83ee36 100644 --- a/app/projects/migrations/0013_auto_20180208_2102.py +++ b/app/projects/migrations/0013_auto_20180208_2102.py @@ -22,6 +22,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='agreementform', name='form_html', - field=models.FileField(upload_to=projects.models.get_agreement_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + field=models.FileField(upload_to=projects.models.models.get_agreement_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), ), ] diff --git a/app/projects/migrations/0013_auto_20180209_1412.py b/app/projects/migrations/0013_auto_20180209_1412.py index 63ccfb26..2921ecaf 100644 --- a/app/projects/migrations/0013_auto_20180209_1412.py +++ b/app/projects/migrations/0013_auto_20180209_1412.py @@ -32,6 +32,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='agreementform', name='form_html', - field=models.FileField(upload_to=projects.models.get_agreement_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + field=models.FileField(upload_to=projects.models.models.get_agreement_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), ), ] diff --git a/app/projects/migrations/0033_dataproject_submission_form_html.py b/app/projects/migrations/0033_dataproject_submission_form_html.py index 4c5fd28b..ddf7e0e0 100644 --- a/app/projects/migrations/0033_dataproject_submission_form_html.py +++ b/app/projects/migrations/0033_dataproject_submission_form_html.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='dataproject', name='submission_form_html', - field=models.FileField(default=None, upload_to=projects.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + field=models.FileField(default=None, upload_to=projects.models.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), preserve_default=False, ), ] diff --git a/app/projects/migrations/0034_auto_20180419_1704.py b/app/projects/migrations/0034_auto_20180419_1704.py index ddde3118..3b6448ce 100644 --- a/app/projects/migrations/0034_auto_20180419_1704.py +++ b/app/projects/migrations/0034_auto_20180419_1704.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='dataproject', name='submission_form_html', - field=models.FileField(blank=True, null=True, upload_to=projects.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), + field=models.FileField(blank=True, null=True, upload_to=projects.models.models.get_submission_form_upload_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])]), ), ] diff --git a/app/projects/migrations/0042_auto_20180515_1729.py b/app/projects/migrations/0042_auto_20180515_1729.py new file mode 100644 index 00000000..6d958db9 --- /dev/null +++ b/app/projects/migrations/0042_auto_20180515_1729.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-05-15 17:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0041_dataproject_has_teams'), + ] + + operations = [ + migrations.CreateModel( + name='PayerDBForm', + fields=[ + ('signedagreementform_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='projects.SignedAgreementForm')), + ('name', models.CharField(max_length=255, null=True, verbose_name='Your name')), + ('title', models.CharField(max_length=100, null=True, verbose_name='Your title')), + ('harvard_address', models.CharField(max_length=255, null=True, verbose_name='Harvard Address')), + ('phone', models.CharField(max_length=255, null=True, verbose_name='Phone Number')), + ('primary_department', models.CharField(max_length=255, null=True, verbose_name='Primary Harvard Department')), + ('specific_aims', models.TextField(null=True, verbose_name="Please provide a concise 'specific aims page style description of the proposed work")), + ('number_team_access', models.CharField(max_length=255, null=True, verbose_name='Approximately how many team members will require access to the HDS database?')), + ('team_sql', models.CharField(max_length=255, null=True, verbose_name='Are the team members familiar with relational databases and the SQL programming language?')), + ('team_r', models.CharField(max_length=255, null=True, verbose_name='Are the team members familiar with the R programming environment?')), + ('team_orchestra', models.CharField(max_length=255, null=True, verbose_name='Are the team members familiar with HMS’ Orchestra cluster computing environment?')), + ('team_windows', models.CharField(max_length=255, null=True, verbose_name='Are the team members familiar with the Windows Server environment and if so, do they have experience working remotely on a Windows platform using Remote Desktop?')), + ('team_analysis', models.CharField(max_length=255, null=True, verbose_name='What is your team’s preferred analysis platform (list all that apply, e.g., R, Matlab, Python, SAS)?')), + ('team_interests', models.CharField(max_length=255, null=True, verbose_name='Please briefly list the primary research interests of your team (e.g., “healthcare economics”, “viral epidemiology”, etc.)')), + ('funding', models.CharField(max_length=255, null=True, verbose_name='Please list any relevant funding sources for this work')), + ('protocol_number', models.CharField(blank=True, max_length=255, null=True, verbose_name='Do you have an existing IRB protocol that covers the proposed work? If so, please provide the protocol number and a copy of the approved proposal.')), + ('signature', models.CharField(max_length=255, null=True, verbose_name='By signing this form, I acknowledge that I have read and agreed to the following terms of use, and will abide by them during the study period.')), + ], + bases=('projects.signedagreementform',), + ), + migrations.CreateModel( + name='PayerDBProject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=20)), + ('funding_status', models.CharField(max_length=250)), + ('specific_aims', models.CharField(max_length=255, null=True, verbose_name='Specific Aims')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='dataproject', + name='agreement_forms_required', + ), + migrations.AddField( + model_name='agreementform', + name='type', + field=models.CharField(blank=True, choices=[('STATIC', 'STATIC'), ('DJANGO', 'DJANGO')], max_length=50, null=True), + ), + ] diff --git a/app/projects/models/__init__.py b/app/projects/models/__init__.py new file mode 100644 index 00000000..228632e8 --- /dev/null +++ b/app/projects/models/__init__.py @@ -0,0 +1,18 @@ +from .models import Institution +from .models import AgreementForm +from .models import DataProject +from .models import DataGate +from .models import SignedAgreementForm +from .models import Team +from .models import Participant +from .models import HostedFile +from .models import HostedFileDownload +from .models import TeamComment +from .models import ParticipantSubmission +from .models import ParticipantProject +from .models import TeamSubmissionsDownload +from .models import AGREEMENT_FORM_TYPE_STATIC +from .models import AGREEMENT_FORM_TYPE_DJANGO + +from .payerdb import PayerDBProject +from .payerdb import PayerDBForm \ No newline at end of file diff --git a/app/projects/models.py b/app/projects/models/models.py similarity index 95% rename from app/projects/models.py rename to app/projects/models/models.py index 01def3fe..291f379b 100644 --- a/app/projects/models.py +++ b/app/projects/models/models.py @@ -9,6 +9,9 @@ EXTERNAL_APP_URL = 'EXTERNAL_APP_URL' S3_BUCKET = 'S3_BUCKET' +AGREEMENT_FORM_TYPE_STATIC = 'STATIC' +AGREEMENT_FORM_TYPE_DJANGO = 'DJANGO' + DATA_LOCATION_TYPE = ( (FILE_SERVICE_URL, 'FileService Signed URL'), (EXTERNAL_APP_URL, 'External Application URL'), @@ -28,6 +31,11 @@ ('R', 'Rejected'), ) +AGREEMENT_FORM_TYPE = ( + (AGREEMENT_FORM_TYPE_STATIC, 'STATIC'), + (AGREEMENT_FORM_TYPE_DJANGO, 'DJANGO') +) + def get_agreement_form_upload_path(instance, filename): form_directory = 'agreementforms/' @@ -70,6 +78,7 @@ class AgreementForm(models.Model): short_name = models.CharField(max_length=6, blank=False, null=False) created = models.DateTimeField(auto_now_add=True) form_file_path = models.CharField(max_length=300, blank=True, null=True) + type = models.CharField(max_length=50, choices=AGREEMENT_FORM_TYPE, blank=True, null=True) def __str__(self): return '%s' % (self.name) @@ -257,6 +266,15 @@ class ParticipantSubmission(models.Model): def __str__(self): return '%s' % (self.uuid) + +class ParticipantProject(models.Model): + name = models.CharField(max_length=20) + funding_status = models.CharField(max_length=250) + + class Meta: + abstract = True + + class TeamSubmissionsDownload(models.Model): """ Tracks who is attempting to download a team's submissions. diff --git a/app/projects/models/payerdb.py b/app/projects/models/payerdb.py new file mode 100644 index 00000000..51580cad --- /dev/null +++ b/app/projects/models/payerdb.py @@ -0,0 +1,40 @@ +from django.db import models + +from .models import ParticipantProject +from .models import SignedAgreementForm + +from django.contrib.auth.models import User + + +class PayerDBForm(SignedAgreementForm): + name = models.CharField(max_length=255, blank=False, null=True, verbose_name="Your name") + title = models.CharField(max_length=100, blank=False, null=True, verbose_name="Your title") + harvard_address = models.CharField(max_length=255, blank=False, null=True, verbose_name="Harvard Address") + phone = models.CharField(max_length=255, blank=False, null=True, verbose_name="Phone Number") + primary_department = models.CharField(max_length=255, blank=False, null=True, verbose_name="Primary Harvard Department") + specific_aims = models.TextField(blank=False, null=True, verbose_name="Please provide a concise 'specific aims page style description of the proposed work") + number_team_access = models.CharField(max_length=255, blank=False, null=True, verbose_name="Approximately how many team members will require access to the HDS database?") + team_sql = models.CharField(max_length=255, blank=False, null=True, verbose_name="Are the team members familiar with relational databases and the SQL programming language?") + team_r = models.CharField(max_length=255, blank=False, null=True, verbose_name="Are the team members familiar with the R programming environment?") + team_orchestra = models.CharField(max_length=255, blank=False, null=True, verbose_name="Are the team members familiar with HMS’ Orchestra cluster computing environment?") + team_windows = models.CharField(max_length=255, blank=False, null=True, verbose_name="Are the team members familiar with the Windows Server " + "environment and if so, do they have experience working " + "remotely on a Windows platform using Remote Desktop?") + team_analysis = models.CharField(max_length=255, blank=False, null=True, verbose_name="What is your team’s preferred analysis platform (list all that" + " apply, e.g., R, Matlab, Python, SAS)?") + team_interests = models.CharField(max_length=255, blank=False, null=True, verbose_name="Please briefly list the primary research interests of " + "your team (e.g., “healthcare economics”, “viral epidemiology”, etc.)") + funding = models.CharField(max_length=255, blank=False, null=True, verbose_name="Please list any relevant funding sources for this work") + protocol_number = models.CharField(max_length=255, blank=True, null=True, verbose_name="Do you have an existing IRB protocol that covers the " + "proposed work? If so, please provide the protocol number " + "and a copy of the approved proposal.") + signature = models.CharField(max_length=255, blank=False, null=True, verbose_name="By signing this form, I acknowledge that I have read " + "and agreed to the following terms of use, and will abide by " + "them during the study period.") + + +class PayerDBProject(ParticipantProject): + specific_aims = models.CharField(max_length=255, blank=False, null=True, verbose_name="Specific Aims") + + def __str__(self): + return '%s' % (self.uuid) \ No newline at end of file diff --git a/app/projects/steps/__init__.py b/app/projects/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/projects/steps/dynamic_form.py b/app/projects/steps/dynamic_form.py new file mode 100644 index 00000000..8e693344 --- /dev/null +++ b/app/projects/steps/dynamic_form.py @@ -0,0 +1,112 @@ +from abc import ABC, abstractmethod +from projects.models import SignedAgreementForm +from projects.models import AGREEMENT_FORM_TYPE_STATIC + +from projects.forms.payerdb import AccessRequestForm + +from projects.models import AgreementForm +from projects.models import DataProject +from projects.models import PayerDBForm + +from datetime import datetime + +class ProjectStep: + + template = None + status = None + agreement_form = None + return_url = None + model_name = None + + def __init__(self, title, project): + self.title = title + self.project = project + + def __str__(self): + return "title : %s project : %s" % (self.title, self.project) + + +def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, user): + agreement_form = AgreementForm.objects.get(id=agreement_form_id) + project = DataProject.objects.get(project_key=project_key) + + payer_db_form_type = agreement_form_factory(model_name) + + payer_db_form = payer_db_form_type(posted_form) + + payer_db_form.save() + + +def agreement_form_factory(form_name, form_input=None): + + if form_name == "payerdb": + if form_input: + return AccessRequestForm + else: + return AccessRequestForm + + return None + + +class ProjectStepInitializer(ABC): + + @abstractmethod + def update_context(self): + pass + + +class SignAgreementFormsStepInitializer(ProjectStepInitializer): + @staticmethod + def get_step_status(current_step, step_name, step_complete): + """ + Returns the status this step should have. If the given step is incomplete and we do not + already have a current_step in context, then this step is the current step and update + context to note this. If this step is incomplete but another step has already been deemed + the current step, then this is a future step. + """ + + if step_complete: + return current_step, 'completed_step' + elif current_step is None: + return step_name, 'current_step' + else: + return current_step, 'future_step' + + def update_context(self, project, user, current_step): + + steps = [] + + agreement_forms = project.agreement_forms.order_by('-name') + + # Each form will be a separate step. + for form in agreement_forms: + # Only include Pending or Approved forms when searching. + signed_forms = SignedAgreementForm.objects.filter( + user=user, + project=project, + agreement_form=form, + status__in=["P", "A"] + ) + + complete = signed_forms.count() > 0 + current_step, status = self.get_step_status(current_step, form.short_name, complete) + + step = ProjectStep(title='Form: {name}'.format(name=form.name), + project=project) + + step.agreement_form = form + + if not form.type or form.type == AGREEMENT_FORM_TYPE_STATIC: + step.template = 'project_signup/sign_agreement_form.html' + else: + step.template = 'project_signup/dynamic_agreement_form.html' + step.form = agreement_form_factory(form.form_file_path) + step.return_url = "projects/%s/" % project.project_key + step.model_name = form.form_file_path + + step.status = status + + steps.append(step) + + return current_step, steps + diff --git a/app/projects/tests.py b/app/projects/tests.py new file mode 100644 index 00000000..d03688db --- /dev/null +++ b/app/projects/tests.py @@ -0,0 +1,44 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from .steps.dynamic_form import SignAgreementFormsStepInitializer +from .steps.dynamic_form import save_dynamic_form + +from .models import AgreementForm +from .models import AGREEMENT_FORM_TYPE_DJANGO +from .models import DataProject + + +class AgreementFormTest(TestCase): + def setUp(self): + super(AgreementFormTest, self).setUp() + + self.super_user = User.objects.create_superuser('rootuser', 'rootuser@thebeatles.com', 'password') + + agreement_form1 = AgreementForm.objects.create(name="AgreementForm1", + short_name="AgreementForm1", + type=AGREEMENT_FORM_TYPE_DJANGO) + + self.test_project_1 = DataProject.objects.create(name="TEST_PROJECT_1", + project_key="TEST_1") + + self.test_project_1.agreement_forms.add(agreement_form1) + + def test_agreement_form_step(self): + + step_initializer = SignAgreementFormsStepInitializer() + + current_step, steps = step_initializer.update_context(project=self.test_project_1, + user=self.super_user, + current_step=None) + assert current_step == "AgreementForm1" + assert steps[0].template == "project_signup/dynamic_agreement_form.html" + + def test_submit_agreement_form(self): + + agreement_form_id = self.test_project_1.agreement_forms.id + project_key = self.test_project_1.project_key + model_name = "payerdb" + submit_form = {"id_name":"TEST_NAME"} + user = self.super_user + + save_dynamic_form(agreement_form_id, project_key, model_name, submit_form, user) \ No newline at end of file diff --git a/app/projects/urls.py b/app/projects/urls.py index 4033bb99..a76854db 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -7,6 +7,7 @@ from .views import manage_contest from .views import signed_agreement_form from .views import save_signed_agreement_form +from .views import save_dynamic_signed_agreement_form from .views import signout from .views import manage_team from .views import DataProjectView @@ -37,6 +38,7 @@ url(r'^manage/(?P[^/]+)/$', manage_contest, name='manage_contest'), url(r'^manage/(?P[^/]+)/(?P[^/]+)/$', manage_team), url(r'^save_signed_agreement_form', save_signed_agreement_form), + url(r'^save_dynamic_signed_agreement_form', save_dynamic_signed_agreement_form), url(r'^signout/$', signout), url(r'^join_team/$', join_team), url(r'^leave_team/$', leave_team), diff --git a/app/projects/views.py b/app/projects/views.py index d70ebc10..d5649341 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -1,44 +1,36 @@ import json import logging -import sys -import requests from datetime import datetime from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.models import User -from django.db.models import Count -from django.shortcuts import render -from django.shortcuts import redirect +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import TemplateView +from hypatio.sciauthz_services import SciAuthZ +from hypatio.scireg_services import get_current_user_profile +from hypatio.scireg_services import get_user_email_confirmation_status +from hypatio.scireg_services import get_user_profile +from profile.forms import RegistrationForm +from projects.steps.dynamic_form import SignAgreementFormsStepInitializer -from pyauth0jwt.auth0authenticate import user_auth_and_jwt +from pyauth0jwt.auth0authenticate import logout_redirect from pyauth0jwt.auth0authenticate import public_user_auth_and_jwt +from pyauth0jwt.auth0authenticate import user_auth_and_jwt from pyauth0jwt.auth0authenticate import validate_request as validate_jwt -from pyauth0jwt.auth0authenticate import logout_redirect - -from .models import DataProject -from .models import Participant -from .models import Team from .models import AgreementForm -from .models import SignedAgreementForm +from .models import DataProject from .models import HostedFile from .models import HostedFileDownload +from .models import Participant +from .models import SignedAgreementForm +from .models import Team from .models import TeamComment -from .models import ParticipantSubmission - -from profile.views import get_task_context_data -from profile.forms import RegistrationForm - -from django.http import HttpResponse -from django.core.exceptions import ObjectDoesNotExist - -from hypatio.sciauthz_services import SciAuthZ -from hypatio.scireg_services import get_user_profile -from hypatio.scireg_services import get_current_user_profile -from hypatio.scireg_services import get_user_email_confirmation_status # Get an instance of a logger logger = logging.getLogger(__name__) @@ -60,6 +52,17 @@ def request_access(request, template_name='dataprojects/access_request.html'): return render(request, template_name, {"project_key": request.POST['project_key'], "agreement_forms": agreement_forms}) + +@user_auth_and_jwt +def save_dynamic_signed_agreement_form(request): + + agreement_form_id = request.POST['agreement_form_id'] + project_key = request.POST['project_key'] + model_name = request.POST['model_name'] + + return HttpResponse(200) + + @user_auth_and_jwt def save_signed_agreement_form(request): @@ -659,31 +662,15 @@ def step_sign_agreement_forms(self, context): if self.project.agreement_forms.count() == 0: return - agreement_forms = self.project.agreement_forms.order_by('-name') - - # Each form will be a separate step. - for form in agreement_forms: - - # Only include Pending or Approved forms when searching. - signed_forms = SignedAgreementForm.objects.filter( - user=self.request.user, - project=self.project, - agreement_form=form, - status__in=["P", "A"] - ) + agreement_form_intializer = SignAgreementFormsStepInitializer() - complete = signed_forms.count() > 0 - status = self.get_step_status(context, form.short_name, complete) + current_step, steps = agreement_form_intializer.update_context(project=self.project, + user=self.request.user, + current_step=context["current_step"]) - # Describe the step. Include here any variables that the template will need. - step = { - 'title': 'Form: {name}'.format(name=form.name), - 'template': 'project_signup/sign_agreement_form.html', - 'status': status, - 'agreement_form': form, - 'project': self.project - } + context["current_step"] = current_step + for step in steps: context['steps'].append(step) def step_show_jwt(self, context): diff --git a/app/templates/dataprojects/__init__.py b/app/templates/dataprojects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/templates/project_signup/dynamic_agreement_form.html b/app/templates/project_signup/dynamic_agreement_form.html new file mode 100644 index 00000000..e12aa223 --- /dev/null +++ b/app/templates/project_signup/dynamic_agreement_form.html @@ -0,0 +1,24 @@ +{% load projects_extras %} +{% load bootstrap3 %} + +
    + {% csrf_token %} + + {{ step.form.render_title | safe }} + + {% bootstrap_form step.form %} + + {% buttons %} + + {% endbuttons %} + + + + + + + +
    + From 15e0a669549db72d4a018b5bf2f8504292126908 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 22 May 2018 09:06:03 -0400 Subject: [PATCH 223/613] TC-219 - Allow saving of dynamic forms. --- app/__init__.py | 0 app/projects/admin.py | 6 ++++ app/projects/steps/dynamic_form.py | 17 +++++----- app/projects/tests.py | 31 ++++++++++++++++--- app/projects/views.py | 6 +++- .../dynamic_agreement_form.html | 2 +- 6 files changed, 47 insertions(+), 15 deletions(-) delete mode 100644 app/__init__.py diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/projects/admin.py b/app/projects/admin.py index 126583a3..454b9dfe 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -10,6 +10,7 @@ from .models import HostedFileDownload from .models import ParticipantSubmission from .models import TeamSubmissionsDownload +from .models import PayerDBForm class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') @@ -44,6 +45,10 @@ class ParticipantSubmissionAdmin(admin.ModelAdmin): class TeamSubmissionsDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'team', 'download_date') +class PayerDBFormAdmin(admin.ModelAdmin): + list_display = ('user', 'agreement_form', 'date_signed', 'status') + + admin.site.register(DataProject, DataprojectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) admin.site.register(SignedAgreementForm, SignedagreementformAdmin) @@ -55,3 +60,4 @@ class TeamSubmissionsDownloadAdmin(admin.ModelAdmin): admin.site.register(HostedFileDownload, HostedFileDownloadAdmin) admin.site.register(ParticipantSubmission, ParticipantSubmissionAdmin) admin.site.register(TeamSubmissionsDownload, TeamSubmissionsDownloadAdmin) +admin.site.register(PayerDBForm, PayerDBFormAdmin) diff --git a/app/projects/steps/dynamic_form.py b/app/projects/steps/dynamic_form.py index 8e693344..f4e351c6 100644 --- a/app/projects/steps/dynamic_form.py +++ b/app/projects/steps/dynamic_form.py @@ -30,20 +30,19 @@ def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, u agreement_form = AgreementForm.objects.get(id=agreement_form_id) project = DataProject.objects.get(project_key=project_key) - payer_db_form_type = agreement_form_factory(model_name) - - payer_db_form = payer_db_form_type(posted_form) - - payer_db_form.save() + dynamic_form_type = agreement_form_factory(model_name) + dynamic_form = dynamic_form_type(posted_form) + dynamic_form_instance = dynamic_form.save(commit=False) + dynamic_form_instance.agreement_form = agreement_form + dynamic_form_instance.user = user + dynamic_form_instance.project = project + dynamic_form_instance.save() def agreement_form_factory(form_name, form_input=None): if form_name == "payerdb": - if form_input: - return AccessRequestForm - else: - return AccessRequestForm + return AccessRequestForm return None diff --git a/app/projects/tests.py b/app/projects/tests.py index d03688db..4535c802 100644 --- a/app/projects/tests.py +++ b/app/projects/tests.py @@ -35,10 +35,33 @@ def test_agreement_form_step(self): def test_submit_agreement_form(self): - agreement_form_id = self.test_project_1.agreement_forms.id + agreement_form_id = self.test_project_1.agreement_forms.first().id project_key = self.test_project_1.project_key model_name = "payerdb" - submit_form = {"id_name":"TEST_NAME"} - user = self.super_user - save_dynamic_form(agreement_form_id, project_key, model_name, submit_form, user) \ No newline at end of file + submit_form = {"agreement_form_id": agreement_form_id, + "model_name": model_name, + "project_key": project_key, + "name": "TEST_NAME", + "title": "TEST_TITLE", + "harvard_address": "TEST_ADDRESS", + "phone": "TEST_PHONE", + "primary_department": "TEST_PRIMARY_DEPARTMENT", + "specific_aims": "TEST_AIMS", + "number_team_access": "TEST_NUM_ACCESS", + "team_sql": "TEST_SQL", + "team_r": "TEST_R", + "team_orchestra": "TEST_ORCHESTRA", + "team_windows": "TEST_WINDOWS", + "team_analysis": "TEST_ANALYSIS", + "team_interests": "TEST_INTERESTS", + "funding": "TEST_FUNDING", + "protocol_number": "TEST_PROTOCOL_NUMBER", + "signature": "TEST_ID_SIGNATURE"} + + save_dynamic_form(agreement_form_id=agreement_form_id, + project_key=project_key, + model_name=model_name, + posted_form=submit_form, + user=self.super_user) + diff --git a/app/projects/views.py b/app/projects/views.py index 196cc8fd..a7f70f3d 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -32,6 +32,8 @@ from .models import Team from .models import TeamComment +from .steps.dynamic_form import save_dynamic_form + # Get an instance of a logger logger = logging.getLogger(__name__) @@ -55,11 +57,13 @@ def request_access(request, template_name='dataprojects/access_request.html'): @user_auth_and_jwt def save_dynamic_signed_agreement_form(request): - + user = request.user agreement_form_id = request.POST['agreement_form_id'] project_key = request.POST['project_key'] model_name = request.POST['model_name'] + save_dynamic_form(agreement_form_id, project_key, model_name, request.POST, user) + return HttpResponse(200) diff --git a/app/templates/project_signup/dynamic_agreement_form.html b/app/templates/project_signup/dynamic_agreement_form.html index e12aa223..e9b7a7dd 100644 --- a/app/templates/project_signup/dynamic_agreement_form.html +++ b/app/templates/project_signup/dynamic_agreement_form.html @@ -1,7 +1,7 @@ {% load projects_extras %} {% load bootstrap3 %} -
    + {% csrf_token %} {{ step.form.render_title | safe }} From 8c140e1748039edbcdeab6ed1b9da76083095165 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 22 May 2018 13:34:04 -0400 Subject: [PATCH 224/613] TC-221 - Adding attributes that trigger ajax submit. --- app/projects/steps/dynamic_form.py | 3 ++- app/projects/tests.py | 7 +++---- app/projects/views.py | 3 ++- app/templates/project_signup/base.html | 4 ++++ .../project_signup/dynamic_agreement_form.html | 14 +++++++++----- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/projects/steps/dynamic_form.py b/app/projects/steps/dynamic_form.py index f4e351c6..6c743bfe 100644 --- a/app/projects/steps/dynamic_form.py +++ b/app/projects/steps/dynamic_form.py @@ -26,7 +26,7 @@ def __str__(self): return "title : %s project : %s" % (self.title, self.project) -def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, user): +def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, user, agreement_text): agreement_form = AgreementForm.objects.get(id=agreement_form_id) project = DataProject.objects.get(project_key=project_key) @@ -34,6 +34,7 @@ def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, u dynamic_form = dynamic_form_type(posted_form) dynamic_form_instance = dynamic_form.save(commit=False) dynamic_form_instance.agreement_form = agreement_form + dynamic_form_instance.agreement_text = agreement_text dynamic_form_instance.user = user dynamic_form_instance.project = project dynamic_form_instance.save() diff --git a/app/projects/tests.py b/app/projects/tests.py index 4535c802..5a39b038 100644 --- a/app/projects/tests.py +++ b/app/projects/tests.py @@ -39,9 +39,7 @@ def test_submit_agreement_form(self): project_key = self.test_project_1.project_key model_name = "payerdb" - submit_form = {"agreement_form_id": agreement_form_id, - "model_name": model_name, - "project_key": project_key, + submit_form = { "name": "TEST_NAME", "title": "TEST_TITLE", "harvard_address": "TEST_ADDRESS", @@ -63,5 +61,6 @@ def test_submit_agreement_form(self): project_key=project_key, model_name=model_name, posted_form=submit_form, - user=self.super_user) + user=self.super_user, + agreement_text="THIS IS AGREEMENT TEXT LA LA
    ") diff --git a/app/projects/views.py b/app/projects/views.py index a7f70f3d..ee9a3825 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -61,8 +61,9 @@ def save_dynamic_signed_agreement_form(request): agreement_form_id = request.POST['agreement_form_id'] project_key = request.POST['project_key'] model_name = request.POST['model_name'] + agreement_text = request.POST['agreement_text'] - save_dynamic_form(agreement_form_id, project_key, model_name, request.POST, user) + save_dynamic_form(agreement_form_id, project_key, model_name, request.POST, user, agreement_text=agreement_text) return HttpResponse(200) diff --git a/app/templates/project_signup/base.html b/app/templates/project_signup/base.html index c3405d41..4f620c0b 100644 --- a/app/templates/project_signup/base.html +++ b/app/templates/project_signup/base.html @@ -100,6 +100,10 @@

    } }); + agreement_form_contents.find("textarea").each(function() { + $(this).replaceWith($(this).val()); + }); + // Find the div element where we want to paste the string containing the completed form var agreement_text_container = $(this).find(".agreement_text"); diff --git a/app/templates/project_signup/dynamic_agreement_form.html b/app/templates/project_signup/dynamic_agreement_form.html index e9b7a7dd..336d4fdc 100644 --- a/app/templates/project_signup/dynamic_agreement_form.html +++ b/app/templates/project_signup/dynamic_agreement_form.html @@ -1,12 +1,17 @@ {% load projects_extras %} {% load bootstrap3 %} - + + {% csrf_token %} - {{ step.form.render_title | safe }} +
    +
    + {{ step.form.render_title | safe }} - {% bootstrap_form step.form %} + {% bootstrap_form step.form %} +
    +
    {% buttons %}

  • From 6c3c8845c5ce228bf47ddc285fd39c94d2e44756 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 31 May 2018 14:36:02 -0400 Subject: [PATCH 226/613] TC-224: Fixes "teams with submissions" count --- app/projects/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/projects/views.py b/app/projects/views.py index e17f4999..60781a0c 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -409,7 +409,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte deleted=False ) - teams_with_any_submission = all_submissions.select_related('participant').select_related('team') + teams_with_any_submission = all_submissions.values('participant__team').distinct() countries = get_distinct_countries_participating(user_jwt, approved_participants, project_key) From 2dae9772b0883c4fbd126a63c503ed800acdd988 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 1 Jun 2018 14:26:00 -0400 Subject: [PATCH 227/613] PPM-228: Adds AUTH0_CLIENT_ID_LIST setting --- app/hypatio/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index ea0bcdae..ba7f06c2 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -120,7 +120,8 @@ SITE_URL = os.environ.get("SITE_URL") AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") -AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") +AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") # TODO remove once py-auth0-jwt(-rest) package(s) are upgraded +AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST") AUTH0_SECRET = os.environ.get("AUTH0_SECRET") AUTH0_SUCCESS_URL = os.environ.get("AUTH0_SUCCESS_URL") AUTH0_LOGOUT_URL = os.environ.get("AUTH0_LOGOUT_URL","") From 1697ac2a1ad808d55c03912df0fb8b7e8220b259 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Mon, 4 Jun 2018 10:47:58 -0400 Subject: [PATCH 228/613] TC-228: Split function needed to turn string into list --- app/hypatio/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index ba7f06c2..97f4e029 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -121,7 +121,7 @@ AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") # TODO remove once py-auth0-jwt(-rest) package(s) are upgraded -AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST") +AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST").split(",") AUTH0_SECRET = os.environ.get("AUTH0_SECRET") AUTH0_SUCCESS_URL = os.environ.get("AUTH0_SUCCESS_URL") AUTH0_LOGOUT_URL = os.environ.get("AUTH0_LOGOUT_URL","") From 5dd055bc39f4c7dcf37249a220abd7d7d0add749 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Tue, 5 Jun 2018 11:28:43 -0400 Subject: [PATCH 229/613] TC-228: Upgrade pyauth0jwt version numbers --- app/hypatio/settings.py | 1 - app/requirements.txt | 4 ++-- gunicorn-nginx-entry.sh | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 97f4e029..14cf99b5 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -120,7 +120,6 @@ SITE_URL = os.environ.get("SITE_URL") AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") -AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") # TODO remove once py-auth0-jwt(-rest) package(s) are upgraded AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST").split(",") AUTH0_SECRET = os.environ.get("AUTH0_SECRET") AUTH0_SUCCESS_URL = os.environ.get("AUTH0_SUCCESS_URL") diff --git a/app/requirements.txt b/app/requirements.txt index 8a0096de..0f539b6e 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,8 +11,8 @@ furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 Pillow==5.0.0 -py-auth0-jwt==0.2.12 -py-auth0-jwt-rest==0.1 +py-auth0-jwt==0.2.14 +py-auth0-jwt-rest==0.1.2 python-pstore==0.8 PyJWT==1.6.1 PyMySQL==0.7.9 diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index b13400f3..979c2a71 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -4,7 +4,7 @@ SITE_URL=$(aws ssm get-parameters --names $PS_PATH.site_url --with-decryption -- ALLOWED_HOSTS=$(aws ssm get-parameters --names $PS_PATH.allowed_hosts --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') DJANGO_SECRET=$(aws ssm get-parameters --names $PS_PATH.django_secret --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_DOMAIN_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_domain --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') -AUTH0_CLIENT_ID_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_client_id --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +AUTH0_CLIENT_ID_LIST=$(aws ssm get-parameters --names $PS_PATH.AUTH0_CLIENT_ID_LIST --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_SECRET_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_secret --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_SUCCESS_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_success_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_LOGOUT_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_logout_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') @@ -25,7 +25,7 @@ export SITE_URL=$SITE_URL export ALLOWED_HOSTS=$ALLOWED_HOSTS export SECRET_KEY=$DJANGO_SECRET export AUTH0_DOMAIN=$AUTH0_DOMAIN_VAULT -export AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID_VAULT +export AUTH0_CLIENT_ID_LIST=$AUTH0_CLIENT_ID_LIST export AUTH0_SECRET=$AUTH0_SECRET_VAULT export AUTH0_SUCCESS_URL=$AUTH0_SUCCESS_URL_VAULT export AUTH0_LOGOUT_URL=$AUTH0_LOGOUT_URL_VAULT From ee78204281183740585fceffb96e3b1bc725adcf Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 6 Jun 2018 09:13:03 -0400 Subject: [PATCH 230/613] TC-228: Lowercased the new secret in the gunicorn file. Might be necessary? --- gunicorn-nginx-entry.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index 979c2a71..f6cd29c4 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -4,7 +4,7 @@ SITE_URL=$(aws ssm get-parameters --names $PS_PATH.site_url --with-decryption -- ALLOWED_HOSTS=$(aws ssm get-parameters --names $PS_PATH.allowed_hosts --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') DJANGO_SECRET=$(aws ssm get-parameters --names $PS_PATH.django_secret --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_DOMAIN_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_domain --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') -AUTH0_CLIENT_ID_LIST=$(aws ssm get-parameters --names $PS_PATH.AUTH0_CLIENT_ID_LIST --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +AUTH0_CLIENT_ID_LIST=$(aws ssm get-parameters --names $PS_PATH.auth0_client_id_list --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_SECRET_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_secret --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_SUCCESS_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_success_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTH0_LOGOUT_URL_VAULT=$(aws ssm get-parameters --names $PS_PATH.auth0_logout_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') From c18c28a0735385b8d3fc1eb4a440f91b8a824cd0 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Mon, 11 Jun 2018 09:51:21 -0400 Subject: [PATCH 231/613] TC-230 - Adding a screen for projects that handle access granting externally. --- .../migrations/0043_auto_20180611_1347.py | 32 +++++++++++++++++ app/projects/models/__init__.py | 2 ++ app/projects/models/models.py | 24 ++++++++++--- app/projects/models/payerdb.py | 14 +++++++- app/projects/steps/dynamic_form.py | 27 ++------------- app/projects/steps/pending_review.py | 15 ++++++++ app/projects/steps/project_step.py | 34 +++++++++++++++++++ app/projects/views.py | 18 ++++++++++ .../project_signup/pending_review.html | 3 ++ 9 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 app/projects/migrations/0043_auto_20180611_1347.py create mode 100644 app/projects/steps/pending_review.py create mode 100644 app/projects/steps/project_step.py create mode 100644 app/templates/project_signup/pending_review.html diff --git a/app/projects/migrations/0043_auto_20180611_1347.py b/app/projects/migrations/0043_auto_20180611_1347.py new file mode 100644 index 00000000..c7d6b33a --- /dev/null +++ b/app/projects/migrations/0043_auto_20180611_1347.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-11 13:47 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0042_auto_20180515_1729'), + ] + + operations = [ + migrations.CreateModel( + name='PayerDBParticipant', + fields=[ + ('participant_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='projects.Participant')), + ('dua_signed', models.BooleanField(default=False)), + ('dua_sign_date', models.DateField()), + ('access_status', models.CharField(max_length=255, null=True, verbose_name='Access Status')), + ('ecommons_id', models.CharField(max_length=255, null=True, verbose_name='eCommons')), + ('ip_address', models.CharField(max_length=255, null=True, verbose_name='IP Address')), + ], + bases=('projects.participant',), + ), + migrations.RemoveField( + model_name='payerdbproject', + name='specific_aims', + ), + ] diff --git a/app/projects/models/__init__.py b/app/projects/models/__init__.py index 228632e8..29b7e4a8 100644 --- a/app/projects/models/__init__.py +++ b/app/projects/models/__init__.py @@ -13,6 +13,8 @@ from .models import TeamSubmissionsDownload from .models import AGREEMENT_FORM_TYPE_STATIC from .models import AGREEMENT_FORM_TYPE_DJANGO +from .models import PERMISSION_SCHEME +from .models import PERMISSION_SCHEME_EXTERNALLY_GRANTED from .payerdb import PayerDBProject from .payerdb import PayerDBForm \ No newline at end of file diff --git a/app/projects/models/models.py b/app/projects/models/models.py index 291f379b..eaaa6037 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -9,15 +9,13 @@ EXTERNAL_APP_URL = 'EXTERNAL_APP_URL' S3_BUCKET = 'S3_BUCKET' -AGREEMENT_FORM_TYPE_STATIC = 'STATIC' -AGREEMENT_FORM_TYPE_DJANGO = 'DJANGO' - DATA_LOCATION_TYPE = ( (FILE_SERVICE_URL, 'FileService Signed URL'), (EXTERNAL_APP_URL, 'External Application URL'), (S3_BUCKET, 'S3 Bucket directly accessed by Hyatio') ) + TEAM_STATUS = ( ('Pending', 'Pending'), ('Ready', 'Ready to be activated'), @@ -25,17 +23,33 @@ ('Deactivated', 'Deactivated') ) + SIGNED_FORM_STATUSES = ( ('P', 'Pending Approval'), ('A', 'Approved'), ('R', 'Rejected'), ) +AGREEMENT_FORM_TYPE_STATIC = 'STATIC' +AGREEMENT_FORM_TYPE_DJANGO = 'DJANGO' + AGREEMENT_FORM_TYPE = ( (AGREEMENT_FORM_TYPE_STATIC, 'STATIC'), (AGREEMENT_FORM_TYPE_DJANGO, 'DJANGO') ) + +PERMISSION_SCHEME_PRIVATE = "PRIVATE" +PERMISSION_SCHEME_PUBLIC = "PUBLIC" +PERMISSION_SCHEME_EXTERNALLY_GRANTED = "EXTERNALLY_GRANTED" + +PERMISSION_SCHEME = ( + (PERMISSION_SCHEME_PRIVATE, "PRIVATE"), + (PERMISSION_SCHEME_PUBLIC, "PUBLIC"), + (PERMISSION_SCHEME_EXTERNALLY_GRANTED, "EXTERNALLY_GRANTED") +) + + def get_agreement_form_upload_path(instance, filename): form_directory = 'agreementforms/' @@ -57,6 +71,7 @@ def get_institution_logo_upload_path(instance, filename): file_extension = filename.split('.')[-1] return '%s/%s.%s' % (form_directory, file_name, file_extension) + class Institution(models.Model): """ This represents an institution such as a university that might be co-sponsoring a challenge. @@ -68,6 +83,7 @@ class Institution(models.Model): def __str__(self): return '%s' % (self.name) + class AgreementForm(models.Model): """ This represents the type of forms that a user might need to sign to be granted access to @@ -83,6 +99,7 @@ class AgreementForm(models.Model): def __str__(self): return '%s' % (self.name) + class DataProject(models.Model): """ This represents a data project that users can access, along with its permissions and requirements. @@ -269,7 +286,6 @@ def __str__(self): class ParticipantProject(models.Model): name = models.CharField(max_length=20) - funding_status = models.CharField(max_length=250) class Meta: abstract = True diff --git a/app/projects/models/payerdb.py b/app/projects/models/payerdb.py index 51580cad..c7ee7407 100644 --- a/app/projects/models/payerdb.py +++ b/app/projects/models/payerdb.py @@ -2,6 +2,7 @@ from .models import ParticipantProject from .models import SignedAgreementForm +from .models import Participant from django.contrib.auth.models import User @@ -33,8 +34,19 @@ class PayerDBForm(SignedAgreementForm): "them during the study period.") +class PayerDBParticipant(Participant): + # Admin facing fields. + dua_signed = models.BooleanField(default=False) + dua_sign_date = models.DateField() + + access_status = models.CharField(max_length=255, blank=False, null=True, verbose_name="Access Status") + ecommons_id = models.CharField(max_length=255, blank=False, null=True, verbose_name="eCommons") + ip_address = models.CharField(max_length=255, blank=False, null=True, verbose_name="IP Address") + + class PayerDBProject(ParticipantProject): - specific_aims = models.CharField(max_length=255, blank=False, null=True, verbose_name="Specific Aims") + + funding_status = models.CharField(max_length=250) def __str__(self): return '%s' % (self.uuid) \ No newline at end of file diff --git a/app/projects/steps/dynamic_form.py b/app/projects/steps/dynamic_form.py index 6c743bfe..e2641322 100644 --- a/app/projects/steps/dynamic_form.py +++ b/app/projects/steps/dynamic_form.py @@ -1,4 +1,3 @@ -from abc import ABC, abstractmethod from projects.models import SignedAgreementForm from projects.models import AGREEMENT_FORM_TYPE_STATIC @@ -6,24 +5,9 @@ from projects.models import AgreementForm from projects.models import DataProject -from projects.models import PayerDBForm -from datetime import datetime - -class ProjectStep: - - template = None - status = None - agreement_form = None - return_url = None - model_name = None - - def __init__(self, title, project): - self.title = title - self.project = project - - def __str__(self): - return "title : %s project : %s" % (self.title, self.project) +from .project_step import ProjectStep +from .project_step import ProjectStepInitializer def save_dynamic_form(agreement_form_id, project_key, model_name, posted_form, user, agreement_text): @@ -48,13 +32,6 @@ def agreement_form_factory(form_name, form_input=None): return None -class ProjectStepInitializer(ABC): - - @abstractmethod - def update_context(self): - pass - - class SignAgreementFormsStepInitializer(ProjectStepInitializer): @staticmethod def get_step_status(current_step, step_name, step_complete): diff --git a/app/projects/steps/pending_review.py b/app/projects/steps/pending_review.py new file mode 100644 index 00000000..845a71b3 --- /dev/null +++ b/app/projects/steps/pending_review.py @@ -0,0 +1,15 @@ +from .project_step import ProjectStepInitializer +from .project_step import ProjectStep + + +class PendingReviewStepInitializer(ProjectStepInitializer): + def update_context(self, project, context): + + status = self.get_step_status(context, "pending_review", False) + + step = ProjectStep(title='Pending Review', + project=project,) + step.template = 'project_signup/pending_review.html' + step.status = status + + return step diff --git a/app/projects/steps/project_step.py b/app/projects/steps/project_step.py new file mode 100644 index 00000000..d136c343 --- /dev/null +++ b/app/projects/steps/project_step.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod + + +class ProjectStep: + + template = None + status = None + agreement_form = None + return_url = None + model_name = None + + def __init__(self, title, project): + self.title = title + self.project = project + + def __str__(self): + return "title : %s project : %s" % (self.title, self.project) + + +class ProjectStepInitializer(ABC): + + @staticmethod + def get_step_status(context, step_name, step_complete): + if step_complete: + return 'completed_step' + elif context['current_step'] is None: + context['current_step'] = step_name + return 'current_step' + else: + return 'future_step' + + @abstractmethod + def update_context(self): + pass diff --git a/app/projects/views.py b/app/projects/views.py index 60781a0c..3a7297cd 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -33,8 +33,10 @@ from .models import SignedAgreementForm from .models import Team from .models import TeamComment +from .models import PERMISSION_SCHEME_EXTERNALLY_GRANTED from .steps.dynamic_form import save_dynamic_form +from projects.steps.pending_review import PendingReviewStepInitializer # Get an instance of a logger logger = logging.getLogger(__name__) @@ -551,6 +553,9 @@ def get_signup_context(self, context): # Team setup step (if needed). self.step_setup_team(context) + # Static page that lets user know to wait. + self.step_pending_review(context) + # Set the template that should be rendered. self.template_name = 'project_signup/base.html' @@ -736,6 +741,19 @@ def step_request_access(self, context): context['steps'].append(step) + def step_pending_review(self, context): + """ + Show a static page letting user know their request is pending. + """ + + if self.project.permission_scheme != PERMISSION_SCHEME_EXTERNALLY_GRANTED: + return + + step = PendingReviewStepInitializer().update_context(project=self.project, + context=context) + + context['steps'].append(step) + def step_setup_team(self, context): """ Builds the context needed for users to create or join a team. This is an diff --git a/app/templates/project_signup/pending_review.html b/app/templates/project_signup/pending_review.html new file mode 100644 index 00000000..bd7de875 --- /dev/null +++ b/app/templates/project_signup/pending_review.html @@ -0,0 +1,3 @@ +{% load static %} + +Your request is being reviewed. Please use the 'Contact Us' link for any further questions. From 7a9b99e3b3f24ae53eb17c511ed8a51a5e9b13c1 Mon Sep 17 00:00:00 2001 From: mtmcduffie Date: Tue, 12 Jun 2018 14:02:54 -0400 Subject: [PATCH 232/613] TC-231 - Adding admin page for managing projects. --- app/hypatio/settings.py | 2 +- .../migrations/0044_auto_20180611_1817.py | 27 ++ .../migrations/0045_auto_20180611_1843.py | 20 ++ .../migrations/0046_auto_20180611_1846.py | 22 ++ app/projects/models/models.py | 12 +- app/projects/models/payerdb.py | 19 +- app/projects/tests.py | 7 + app/projects/urls.py | 8 +- app/projects/views.py | 203 ++---------- app/projects/views_manage.py | 302 ++++++++++++++++++ .../manage/manage_project_teams.html | 288 +++++++++++++++++ app/templates/manage/manageprojects.html | 65 ++++ .../shared/dynamic_signed_agreement_form.html | 46 +++ 13 files changed, 841 insertions(+), 180 deletions(-) create mode 100644 app/projects/migrations/0044_auto_20180611_1817.py create mode 100644 app/projects/migrations/0045_auto_20180611_1843.py create mode 100644 app/projects/migrations/0046_auto_20180611_1846.py create mode 100644 app/projects/views_manage.py create mode 100644 app/templates/manage/manage_project_teams.html create mode 100644 app/templates/manage/manageprojects.html create mode 100644 app/templates/shared/dynamic_signed_agreement_form.html diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 14cf99b5..745e9bba 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -120,7 +120,7 @@ SITE_URL = os.environ.get("SITE_URL") AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") -AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST").split(",") +AUTH0_CLIENT_ID_LIST = os.environ.get("AUTH0_CLIENT_ID_LIST","").split(",") AUTH0_SECRET = os.environ.get("AUTH0_SECRET") AUTH0_SUCCESS_URL = os.environ.get("AUTH0_SUCCESS_URL") AUTH0_LOGOUT_URL = os.environ.get("AUTH0_LOGOUT_URL","") diff --git a/app/projects/migrations/0044_auto_20180611_1817.py b/app/projects/migrations/0044_auto_20180611_1817.py new file mode 100644 index 00000000..1ba3c118 --- /dev/null +++ b/app/projects/migrations/0044_auto_20180611_1817.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-11 18:17 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0043_auto_20180611_1347'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='team_leader', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='team', + unique_together=set([('team_leader', 'data_project')]), + ), + ] diff --git a/app/projects/migrations/0045_auto_20180611_1843.py b/app/projects/migrations/0045_auto_20180611_1843.py new file mode 100644 index 00000000..c698afed --- /dev/null +++ b/app/projects/migrations/0045_auto_20180611_1843.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-11 18:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0044_auto_20180611_1817'), + ] + + operations = [ + migrations.AlterField( + model_name='payerdbparticipant', + name='dua_sign_date', + field=models.DateField(null=True), + ), + ] diff --git a/app/projects/migrations/0046_auto_20180611_1846.py b/app/projects/migrations/0046_auto_20180611_1846.py new file mode 100644 index 00000000..a35c8f2d --- /dev/null +++ b/app/projects/migrations/0046_auto_20180611_1846.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-11 18:46 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0045_auto_20180611_1843'), + ] + + operations = [ + migrations.AlterField( + model_name='participant', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/projects/models/models.py b/app/projects/models/models.py index eaaa6037..ca882283 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -131,11 +131,13 @@ class DataProject(models.Model): def __str__(self): return '%s' % (self.project_key) + class DataGate(models.Model): project = models.ForeignKey(DataProject) data_location_type = models.CharField(max_length=50, choices=DATA_LOCATION_TYPE) data_location = models.CharField(max_length=250) + class SignedAgreementForm(models.Model): """ This represents the fully signed agreement form. @@ -147,11 +149,16 @@ class SignedAgreementForm(models.Model): agreement_text = models.TextField(blank=False) status = models.CharField(max_length=1, null=False, blank=False, default='P', choices=SIGNED_FORM_STATUSES) + class Team(models.Model): + + class Meta: + unique_together = ('team_leader', 'data_project',) + """ This model describes a team of participants that are competing in a data challenge. """ - team_leader = models.OneToOneField(User) + team_leader = models.ForeignKey(User) data_project = models.ForeignKey(DataProject) status = models.CharField(max_length=30, choices=TEAM_STATUS, default='Pending') @@ -186,8 +193,9 @@ def get_submissions(self): def __str__(self): return '%s' % self.team_leader.email + class Participant(models.Model): - user = models.OneToOneField(User) + user = models.ForeignKey(User) data_challenge = models.ForeignKey(DataProject) team = models.ForeignKey(Team, null=True, blank=True, on_delete=models.CASCADE) team_wait_on_leader_email = models.CharField(max_length=100, blank=True, null=True) diff --git a/app/projects/models/payerdb.py b/app/projects/models/payerdb.py index c7ee7407..1666739f 100644 --- a/app/projects/models/payerdb.py +++ b/app/projects/models/payerdb.py @@ -1,10 +1,11 @@ from django.db import models +from django.dispatch import receiver from .models import ParticipantProject from .models import SignedAgreementForm from .models import Participant -from django.contrib.auth.models import User +from ..models import Team class PayerDBForm(SignedAgreementForm): @@ -34,10 +35,24 @@ class PayerDBForm(SignedAgreementForm): "them during the study period.") +@receiver(models.signals.post_save, sender=PayerDBForm) +def execute_after_save(sender, instance, created, *args, **kwargs): + if created: + #TODO: Catch error if user already created team. + new_team = Team(team_leader=instance.user, + data_project=instance.project) + new_team.save() + + new_participant = PayerDBParticipant(user=instance.user, + data_challenge=instance.project, + team = new_team) + new_participant.save() + + class PayerDBParticipant(Participant): # Admin facing fields. dua_signed = models.BooleanField(default=False) - dua_sign_date = models.DateField() + dua_sign_date = models.DateField(null=True) access_status = models.CharField(max_length=255, blank=False, null=True, verbose_name="Access Status") ecommons_id = models.CharField(max_length=255, blank=False, null=True, verbose_name="eCommons") diff --git a/app/projects/tests.py b/app/projects/tests.py index 5a39b038..4aa2a358 100644 --- a/app/projects/tests.py +++ b/app/projects/tests.py @@ -4,8 +4,10 @@ from .steps.dynamic_form import save_dynamic_form from .models import AgreementForm +from .models import PayerDBForm from .models import AGREEMENT_FORM_TYPE_DJANGO from .models import DataProject +from .models import Team class AgreementFormTest(TestCase): @@ -64,3 +66,8 @@ def test_submit_agreement_form(self): user=self.super_user, agreement_text="THIS IS AGREEMENT TEXT LA LA
    ") + # Did we save the form okay? + assert PayerDBForm.objects.filter(name="TEST_NAME").exists() + + # Did we create the team? + assert Team.objects.filter(team_leader=self.super_user).exists() diff --git a/app/projects/urls.py b/app/projects/urls.py index a76854db..6a43ccd5 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -4,12 +4,14 @@ from .views import list_data_challenges from .views import request_access from .views import submit_user_permission_request -from .views import manage_contest +from .views_manage import manage_contest +from .views_manage import manage_project +from .views_manage import manage_project_team from .views import signed_agreement_form from .views import save_signed_agreement_form from .views import save_dynamic_signed_agreement_form from .views import signout -from .views import manage_team +from .views_manage import manage_team from .views import DataProjectView from .views_teams import join_team @@ -37,6 +39,8 @@ url(r'^submit_user_permission_request/$', submit_user_permission_request), url(r'^manage/(?P[^/]+)/$', manage_contest, name='manage_contest'), url(r'^manage/(?P[^/]+)/(?P[^/]+)/$', manage_team), + url(r'^manage_project/(?P[^/]+)/$', manage_project, name='manage_project'), + url(r'^manage_project/(?P[^/]+)/(?P[^/]+)/$', manage_project_team), url(r'^save_signed_agreement_form', save_signed_agreement_form), url(r'^save_dynamic_signed_agreement_form', save_dynamic_signed_agreement_form), url(r'^signout/$', signout), diff --git a/app/projects/views.py b/app/projects/views.py index 3a7297cd..5e1ee897 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth import logout -from django.contrib.auth.models import User + from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse from django.shortcuts import get_object_or_404 @@ -15,8 +15,7 @@ from hypatio.sciauthz_services import SciAuthZ from hypatio.scireg_services import get_current_user_profile from hypatio.scireg_services import get_user_email_confirmation_status -from hypatio.scireg_services import get_user_profile -from hypatio.scireg_services import get_distinct_countries_participating + from profile.forms import RegistrationForm from projects.steps.dynamic_form import SignAgreementFormsStepInitializer @@ -26,16 +25,16 @@ from pyauth0jwt.auth0authenticate import validate_request as validate_jwt from .models import AgreementForm from .models import DataProject -from .models import HostedFile -from .models import HostedFileDownload + from .models import Participant -from .models import ParticipantSubmission from .models import SignedAgreementForm -from .models import Team -from .models import TeamComment +from .models import AGREEMENT_FORM_TYPE_STATIC + from .models import PERMISSION_SCHEME_EXTERNALLY_GRANTED from .steps.dynamic_form import save_dynamic_form +from .steps.dynamic_form import agreement_form_factory + from projects.steps.pending_review import PendingReviewStepInitializer # Get an instance of a logger @@ -109,7 +108,7 @@ def submit_user_permission_request(request): @user_auth_and_jwt -def signed_agreement_form(request, template_name='shared/signed_agreement_form.html'): +def signed_agreement_form(request): project_key = request.GET['project_key'] signed_agreement_form_id = request.GET['signed_form_id'] @@ -123,10 +122,28 @@ def signed_agreement_form(request, template_name='shared/signed_agreement_form.h participant = Participant.objects.get(data_challenge=project, user=signed_form.user) if is_manager or signed_form.user == request.user: + + if not signed_form.agreement_form.type or signed_form.agreement_form.type == AGREEMENT_FORM_TYPE_STATIC: + template_name = "shared/signed_agreement_form.html" + filled_out_signed_form = None + else: + template_name = "shared/dynamic_signed_agreement_form.html" + + # We need to get both the type of form, and the model underlying that form, dynamically. + # Get an instance of the form based on file path. + form_object = agreement_form_factory(signed_form.agreement_form.form_file_path) + + # Get an instance of the model that was saved from the form. + filled_out_form_instance = get_object_or_404(form_object._meta.model, signedagreementform_ptr_id=signed_agreement_form_id) + + # Populate the form with data from the model so we can render it with django bootstrap. + filled_out_signed_form = form_object(instance=filled_out_form_instance) + return render(request, template_name, {"user": request.user, "ssl_setting": settings.SSL_SETTING, "is_manager": is_manager, "signed_form": signed_form, + "filled_out_signed_form": filled_out_signed_form, "participant": participant}) else: return HttpResponse(403) @@ -267,170 +284,6 @@ def list_data_challenges(request, template_name='datacontests/list.html'): "profile_server_url": settings.SCIREG_SERVER_URL}) -@user_auth_and_jwt -def manage_team(request, project_key, team_leader, template_name='datacontests/manageteams.html'): - """ - Populates the team management modal popup on the contest management screen. - """ - - user = request.user - user_jwt = request.COOKIES.get("DBMI_JWT", None) - - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission(project_key) - - if not is_manager: - logger.debug('[HYPATIO][DEBUG][manage_team] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') - return HttpResponse(403) - - project = DataProject.objects.get(project_key=project_key) - team = Team.objects.get(data_project=project, team_leader__email=team_leader) - num_required_forms = project.agreement_forms.count() - - user_jwt = request.COOKIES.get("DBMI_JWT", None) - - # Collect all the team member information needed - team_member_details = [] - team_participants = team.participant_set.all() - team_accepted_forms = 0 - - for member in team_participants: - email = member.user.email - - # Make a request to SciReg for a specific person's user information - user_info_json = get_user_profile(user_jwt, email, project_key) - - if user_info_json['count'] != 0: - user_info = user_info_json["results"][0] - else: - user_info = None - - signed_agreement_forms = [] - signed_accepted_agreement_forms = 0 - - # For each of the available agreement forms for this project, display only latest version completed by the user - for agreement_form in project.agreement_forms.all(): - signed_form = SignedAgreementForm.objects.filter(user__email=email, - project=project, - agreement_form=agreement_form).last() - - if signed_form is not None: - signed_agreement_forms.append(signed_form) - - if signed_form.status == 'A': - team_accepted_forms += 1 - signed_accepted_agreement_forms += 1 - - team_member_details.append({ - 'email': email, - 'user_info': user_info, - 'signed_agreement_forms': signed_agreement_forms, - 'signed_accepted_agreement_forms': signed_accepted_agreement_forms, - 'participant': member - }) - - # Check whether this team has completed all the necessary forms and they have been accepted by challenge admins - total_required_forms_for_team = project.agreement_forms.count() * team_participants.count() - team_has_all_forms_complete = total_required_forms_for_team == team_accepted_forms - - institution = project.institution - - # Get the comments made about this team by challenge administrators - comments = TeamComment.objects.filter(team=team) - - # Get a history of files downloaded and uploaded by members of this team - files = HostedFile.objects.filter(project=project) - team_users = User.objects.filter(participant__in=team_participants) - downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) - uploads = team.get_submissions() - - return render(request, template_name, context={"user": user, - "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, - "project": project, - "team": team, - "team_members": team_member_details, - "num_required_forms": num_required_forms, - "team_has_all_forms_complete": team_has_all_forms_complete, - "institution": institution, - "comments": comments, - "downloads": downloads, - "uploads": uploads}) - - -@user_auth_and_jwt -def manage_contest(request, project_key, template_name='datacontests/managecontests.html'): - - project = DataProject.objects.get(project_key=project_key) - - # This dictionary will hold all user requests and permissions - user_details = {} - - user = request.user - user_jwt = request.COOKIES.get("DBMI_JWT", None) - - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission(project_key) - - if not is_manager: - logger.debug('[HYPATIO][DEBUG][manage_contest] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') - return HttpResponse(403) - - teams = Team.objects.filter(data_project=project) - - # A person who has filled out a form for a project but not yet joined a team - users_with_a_team = Participant.objects.filter(team__in=teams).values_list('user', flat=True).distinct() - users_who_signed_forms = SignedAgreementForm.objects.filter(project=project).values_list('user', flat=True).distinct() - users_without_a_team = User.objects.filter(id__in=users_who_signed_forms).exclude(id__in=users_with_a_team) - - # Collect additional information about these participants who aren't on teams yet - users_without_a_team_details = [] - - for person in users_without_a_team: - email = person.email - - signed_agreement_forms = [] - - # For each of the available agreement forms for this project, display only latest version completed by the user - for agreement_form in project.agreement_forms.all(): - signed_agreement_forms.append(SignedAgreementForm.objects.filter(user__email=email, project=project, agreement_form=agreement_form).last()) - - users_without_a_team_details.append({ - 'email': email, - # 'user_info': user_info, - 'signed_agreement_forms': signed_agreement_forms, - 'participant': person - }) - - approved_teams = teams.filter(status='Active') - - approved_participants = Participant.objects.filter(team__in=approved_teams) - - all_submissions = ParticipantSubmission.objects.filter( - participant__in=approved_participants, - deleted=False - ) - - teams_with_any_submission = all_submissions.values('participant__team').distinct() - - countries = get_distinct_countries_participating(user_jwt, approved_participants, project_key) - - institution = project.institution - - return render(request, template_name, {"user": user, - "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, - "project": project, - "teams": teams, - "users_without_a_team_details": users_without_a_team_details, - "approved_teams": approved_teams.count(), - "approved_participants": approved_participants.count(), - "total_submissions": all_submissions.count(), - "teams_with_any_submission": teams_with_any_submission.count(), - "participating_countries": countries, - "institution": institution}) - - @method_decorator(public_user_auth_and_jwt, name='dispatch') class DataProjectView(TemplateView): """ @@ -916,6 +769,10 @@ def is_user_granted_access(self, context): if not context['has_view_permission'] and not context['is_manager']: return False + # If the permission is managed outside this project, return false + if self.project.permission_scheme == PERMISSION_SCHEME_EXTERNALLY_GRANTED: + return False + # Additional requirements if a DataProject requires teams. if self.project.has_teams: diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py new file mode 100644 index 00000000..c5257797 --- /dev/null +++ b/app/projects/views_manage.py @@ -0,0 +1,302 @@ +import logging + +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import render +from django.contrib.auth.models import User + +from pyauth0jwt.auth0authenticate import user_auth_and_jwt + +from hypatio.sciauthz_services import SciAuthZ + +from .models import DataProject +from .models import Team +from .models import TeamComment +from .models import SignedAgreementForm +from .models import HostedFile +from .models import HostedFileDownload +from .models import Participant +from .models import ParticipantSubmission + +from hypatio.scireg_services import get_distinct_countries_participating +from hypatio.scireg_services import get_user_profile + +# Get an instance of a logger +logger = logging.getLogger(__name__) + + +@user_auth_and_jwt +def manage_project(request, project_key, template_name='manage/manageprojects.html'): + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + project = DataProject.objects.get(project_key=project_key) + + if not is_manager: + logger.debug( + '[HYPATIO][DEBUG][manage_team] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) + + teams = Team.objects.filter(data_project=project) + + return render(request, template_name, context={"user": user, + "is_manager": is_manager, + "project": project, + "teams": teams, + }) + +#TODO: Use this more widely. +def prepare_participant_details(participants, user_jwt, project): + + team_member_details = [] + team_accepted_forms = 0 + + for member in participants: + email = member.user.email + + # Make a request to SciReg for a specific person's user information + user_info_json = get_user_profile(user_jwt, email, project.project_key) + + if user_info_json['count'] != 0: + user_info = user_info_json["results"][0] + else: + user_info = None + + signed_agreement_forms = [] + signed_accepted_agreement_forms = 0 + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + signed_form = SignedAgreementForm.objects.filter(user__email=email, + project=project, + agreement_form=agreement_form).last() + + if signed_form is not None: + signed_agreement_forms.append(signed_form) + + if signed_form.status == 'A': + team_accepted_forms += 1 + signed_accepted_agreement_forms += 1 + + team_member_details.append({ + 'email': email, + 'user_info': user_info, + 'signed_agreement_forms': signed_agreement_forms, + 'signed_accepted_agreement_forms': signed_accepted_agreement_forms, + 'participant': member + }) + + return team_member_details, team_accepted_forms + + +@user_auth_and_jwt +def manage_project_team(request, project_key, team_leader, template_name='manage/manage_project_teams.html'): + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug( + '[HYPATIO][DEBUG][manage_team] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) + + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(data_project=project, team_leader__email=team_leader) + num_required_forms = project.agreement_forms.count() + + # Collect all the team member information needed + team_participants = team.participant_set.all() + team_member_details, team_accepted_forms = prepare_participant_details(team_participants, user_jwt, project) + + # Check whether this team has completed all the necessary forms and they have been accepted by challenge admins + total_required_forms_for_team = project.agreement_forms.count() * team_participants.count() + team_has_all_forms_complete = total_required_forms_for_team == team_accepted_forms + + institution = project.institution + + # Get the comments made about this team by challenge administrators + comments = TeamComment.objects.filter(team=team) + + return render(request, template_name, context={"user": user, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "project": project, + "team": team, + "team_members": team_member_details, + "num_required_forms": num_required_forms, + "team_has_all_forms_complete": team_has_all_forms_complete, + "institution": institution, + "comments": comments}) + + +@user_auth_and_jwt +def manage_team(request, project_key, team_leader, template_name='datacontests/manageteams.html'): + """ + Populates the team management modal popup on the contest management screen. + """ + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug( + '[HYPATIO][DEBUG][manage_team] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) + + project = DataProject.objects.get(project_key=project_key) + team = Team.objects.get(data_project=project, team_leader__email=team_leader) + num_required_forms = project.agreement_forms.count() + + # Collect all the team member information needed + team_member_details = [] + team_participants = team.participant_set.all() + team_accepted_forms = 0 + + for member in team_participants: + email = member.user.email + + # Make a request to SciReg for a specific person's user information + user_info_json = get_user_profile(user_jwt, email, project_key) + + if user_info_json['count'] != 0: + user_info = user_info_json["results"][0] + else: + user_info = None + + signed_agreement_forms = [] + signed_accepted_agreement_forms = 0 + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + signed_form = SignedAgreementForm.objects.filter(user__email=email, + project=project, + agreement_form=agreement_form).last() + + if signed_form is not None: + signed_agreement_forms.append(signed_form) + + if signed_form.status == 'A': + team_accepted_forms += 1 + signed_accepted_agreement_forms += 1 + + team_member_details.append({ + 'email': email, + 'user_info': user_info, + 'signed_agreement_forms': signed_agreement_forms, + 'signed_accepted_agreement_forms': signed_accepted_agreement_forms, + 'participant': member + }) + + # Check whether this team has completed all the necessary forms and they have been accepted by challenge admins + total_required_forms_for_team = project.agreement_forms.count() * team_participants.count() + team_has_all_forms_complete = total_required_forms_for_team == team_accepted_forms + + institution = project.institution + + # Get the comments made about this team by challenge administrators + comments = TeamComment.objects.filter(team=team) + + # Get a history of files downloaded and uploaded by members of this team + files = HostedFile.objects.filter(project=project) + team_users = User.objects.filter(participant__in=team_participants) + downloads = HostedFileDownload.objects.filter(hosted_file__in=files, user__in=team_users) + uploads = team.get_submissions() + + return render(request, template_name, context={"user": user, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "project": project, + "team": team, + "team_members": team_member_details, + "num_required_forms": num_required_forms, + "team_has_all_forms_complete": team_has_all_forms_complete, + "institution": institution, + "comments": comments, + "downloads": downloads, + "uploads": uploads}) + + +@user_auth_and_jwt +def manage_contest(request, project_key, template_name='datacontests/managecontests.html'): + project = DataProject.objects.get(project_key=project_key) + + # This dictionary will hold all user requests and permissions + user_details = {} + + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug( + '[HYPATIO][DEBUG][manage_contest] User ' + user.email + ' does not have MANAGE permissions for item ' + project_key + '.') + return HttpResponse(403) + + teams = Team.objects.filter(data_project=project) + + # A person who has filled out a form for a project but not yet joined a team + users_with_a_team = Participant.objects.filter(team__in=teams).values_list('user', flat=True).distinct() + users_who_signed_forms = SignedAgreementForm.objects.filter(project=project).values_list('user', + flat=True).distinct() + users_without_a_team = User.objects.filter(id__in=users_who_signed_forms).exclude(id__in=users_with_a_team) + + # Collect additional information about these participants who aren't on teams yet + users_without_a_team_details = [] + + for person in users_without_a_team: + email = person.email + + signed_agreement_forms = [] + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + signed_agreement_forms.append(SignedAgreementForm.objects.filter(user__email=email, project=project, + agreement_form=agreement_form).last()) + + users_without_a_team_details.append({ + 'email': email, + # 'user_info': user_info, + 'signed_agreement_forms': signed_agreement_forms, + 'participant': person + }) + + approved_teams = teams.filter(status='Active') + + approved_participants = Participant.objects.filter(team__in=approved_teams) + + all_submissions = ParticipantSubmission.objects.filter( + participant__in=approved_participants, + deleted=False + ) + + teams_with_any_submission = all_submissions.values('participant__team').distinct() + + countries = get_distinct_countries_participating(user_jwt, approved_participants, project_key) + + institution = project.institution + + return render(request, template_name, {"user": user, + "ssl_setting": settings.SSL_SETTING, + "is_manager": is_manager, + "project": project, + "teams": teams, + "users_without_a_team_details": users_without_a_team_details, + "approved_teams": approved_teams.count(), + "approved_participants": approved_participants.count(), + "total_submissions": all_submissions.count(), + "teams_with_any_submission": teams_with_any_submission.count(), + "participating_countries": countries, + "institution": institution}) + diff --git a/app/templates/manage/manage_project_teams.html b/app/templates/manage/manage_project_teams.html new file mode 100644 index 00000000..f9491d3c --- /dev/null +++ b/app/templates/manage/manage_project_teams.html @@ -0,0 +1,288 @@ +{% extends 'base.html' %} +{% load countries %} +{% load tz %} + +{% block headscripts %} + + + + +{% endblock %} + +{% block tab_name %}Team Management{% endblock %} +{% block title %}{{ project.project_key }} Team Management{% endblock %} +{% block subtitle %}Team leader: {{ team.team_leader }}{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +

    Team members

    +
    +
    + + + + + + + + + + + + {% for member in team_members %} + + + + + + + + {% endfor %} + +
    NameLocationEmailStatusSigned Forms
    {{ member.user_info.first_name }} {{ member.user_info.last_name }} + {% get_country member.user_info.country as country %} + {% if country == 'US' and member.user_info.state != "" %} + {{ member.user_info.state }}, USA + {% else %} + {{ country.name }} + {% endif %} + + {{ member.email }} + {% if member.email == team.team_leader.email %} + Team Leader + {% endif %} + + {% if member.participant.team_pending %} + Pending team leader approval + {% elif member.signed_accepted_agreement_forms != num_required_forms %} + Pending forms + {% else %} + No issues + {% endif %} + + {% for form in member.signed_agreement_forms %} + + {{ form.agreement_form.short_name }} + + {% endfor %} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    Admin comments

    +
    +
    +
    + + +
    + + +
    + +

    Previous comments:

    + + {% for comment in comments %} +
    +
    {{ comment.user.email }}, {{ comment.date|timezone:"America/New_York" }} (EST)
    +
    {{ comment.text }}
    +
    + {% endfor %} +
    +
    +
    +
    +{% endblock %} + +{% block footerscripts %} + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/manage/manageprojects.html b/app/templates/manage/manageprojects.html new file mode 100644 index 00000000..c48c2b23 --- /dev/null +++ b/app/templates/manage/manageprojects.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} +{% load countries %} + +{% block headscripts %} + + + + +{% endblock %} + +{% block title %}{{ project.project_key }} Management Page{% endblock %} + +{% block content %} +
    +
    +
    + +
    + + + + + + + + + {% for team in teams %} + + + + + {% endfor %} + +
    TeamMembers
    {{ team }}{{ team.participant_set.all.count }}
    +
    +
    +
    +
    +{% endblock %} + +{% block footerscripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/shared/dynamic_signed_agreement_form.html b/app/templates/shared/dynamic_signed_agreement_form.html new file mode 100644 index 00000000..9794d710 --- /dev/null +++ b/app/templates/shared/dynamic_signed_agreement_form.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% load tz %} +{% load bootstrap3 %} + +{% block headscripts %} +{% endblock %} + +{% block tab_name %}Signed Agreement Form{% endblock %} +{% block title %}Manage Signed Agreement Form{% endblock %} +{% block subtitle %}{{ signed_form.agreement_form.name }} -- {{ signed_form.user }}{% endblock %} + +{% block content %} +
    +
    +
    + +
    + {{ signed_form.render_title | safe }} + + {% bootstrap_form filled_out_signed_form %} +
    +
    +
    + +
    +
    + +
    +

    Form Status: {{ signed_form.get_status_display }}

    +

    Signed by: {{ signed_form.user }}

    +

    Member of team: {{ participant.team.team_leader }}

    +

    For Project: {{ signed_form.project.name }}

    +

    Form Name: {{ signed_form.agreement_form.name }}

    +

    Signed on: {{ signed_form.date_signed|timezone:"America/New_York" }} (EST)

    +
    +
    +
    +
    +{% endblock %} + + + From 26d2078c05eee20bfbd1f8c4ddc2aa7bed991b24 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 21 Jun 2018 13:41:14 -0400 Subject: [PATCH 233/613] HYP-1: Model change to allow data projects to share the same name Needed since all N2C2 tracks have the same primary name. --- .../migrations/0047_auto_20180621_1721.py | 20 +++++++++++++++++++ app/projects/models/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 app/projects/migrations/0047_auto_20180621_1721.py diff --git a/app/projects/migrations/0047_auto_20180621_1721.py b/app/projects/migrations/0047_auto_20180621_1721.py new file mode 100644 index 00000000..b9fdc79a --- /dev/null +++ b/app/projects/migrations/0047_auto_20180621_1721.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-21 17:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0046_auto_20180611_1846'), + ] + + operations = [ + migrations.AlterField( + model_name='dataproject', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Name of project'), + ), + ] diff --git a/app/projects/models/models.py b/app/projects/models/models.py index ca882283..56f69d05 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -106,7 +106,7 @@ class DataProject(models.Model): A DataProject can be simply a data set or it can be a data contest as recognized by the is_contest flag. The submission form file should be an html file that lives under static/submissionforms/. """ - name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Name of project", unique=True) + name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Name of project", unique=False) project_key = models.CharField(max_length=100, blank=True, null=True, verbose_name="Project Key", unique=True) institution = models.ForeignKey(Institution, blank=True, null=True, on_delete=models.PROTECT) description = models.TextField(blank=True, null=True, verbose_name="Description") From 301978f57baed1ec523ba4b81f69c365aa028a6d Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 22 Jun 2018 15:52:39 -0400 Subject: [PATCH 234/613] HYP-3: Externally linked Agreement Forms Needed for N2C2 Track 2 which uses the MIMIC form on a 3rd party website. Commit includes changes to step completeness logic to make this kind of form always be displayed once that step is reached. --- app/projects/admin.py | 2 +- .../migrations/0048_auto_20180621_1810.py | 25 +++++++++++ .../0049_agreementform_description.py | 20 +++++++++ app/projects/models/__init__.py | 1 + app/projects/models/models.py | 20 +++++++-- app/projects/steps/dynamic_form.py | 15 +++++-- app/projects/urls.py | 2 + app/projects/views.py | 44 +++++++++++++++++-- app/templates/base.html | 2 +- app/templates/project_signup/base.html | 4 +- .../external_agreement_form.html | 38 ++++++++++++++++ 11 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 app/projects/migrations/0048_auto_20180621_1810.py create mode 100644 app/projects/migrations/0049_agreementform_description.py create mode 100644 app/templates/project_signup/external_agreement_form.html diff --git a/app/projects/admin.py b/app/projects/admin.py index 454b9dfe..a7308f31 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -16,7 +16,7 @@ class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') class AgreementformAdmin(admin.ModelAdmin): - list_display = ('name', 'short_name', 'form_file_path') + list_display = ('name', 'short_name', 'type', 'form_file_path') class SignedagreementformAdmin(admin.ModelAdmin): list_display = ('user', 'agreement_form', 'date_signed', 'status') diff --git a/app/projects/migrations/0048_auto_20180621_1810.py b/app/projects/migrations/0048_auto_20180621_1810.py new file mode 100644 index 00000000..ed357090 --- /dev/null +++ b/app/projects/migrations/0048_auto_20180621_1810.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-21 18:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0047_auto_20180621_1721'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='external_link', + field=models.CharField(blank=True, max_length=300, null=True), + ), + migrations.AlterField( + model_name='agreementform', + name='type', + field=models.CharField(blank=True, choices=[('STATIC', 'STATIC'), ('DJANGO', 'DJANGO'), ('EXTERNAL_LINK', 'EXTERNAL LINK')], max_length=50, null=True), + ), + ] diff --git a/app/projects/migrations/0049_agreementform_description.py b/app/projects/migrations/0049_agreementform_description.py new file mode 100644 index 00000000..381a6446 --- /dev/null +++ b/app/projects/migrations/0049_agreementform_description.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-06-21 18:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0048_auto_20180621_1810'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='description', + field=models.TextField(blank=True), + ), + ] diff --git a/app/projects/models/__init__.py b/app/projects/models/__init__.py index 29b7e4a8..077efe24 100644 --- a/app/projects/models/__init__.py +++ b/app/projects/models/__init__.py @@ -13,6 +13,7 @@ from .models import TeamSubmissionsDownload from .models import AGREEMENT_FORM_TYPE_STATIC from .models import AGREEMENT_FORM_TYPE_DJANGO +from .models import AGREEMENT_FORM_TYPE_EXTERNAL_LINK from .models import PERMISSION_SCHEME from .models import PERMISSION_SCHEME_EXTERNALLY_GRANTED diff --git a/app/projects/models/models.py b/app/projects/models/models.py index 56f69d05..cfd78f98 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -2,9 +2,9 @@ from django.db import models from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.core.validators import FileExtensionValidator - FILE_SERVICE_URL = 'FILE_SERVICE_URL' EXTERNAL_APP_URL = 'EXTERNAL_APP_URL' S3_BUCKET = 'S3_BUCKET' @@ -32,10 +32,12 @@ AGREEMENT_FORM_TYPE_STATIC = 'STATIC' AGREEMENT_FORM_TYPE_DJANGO = 'DJANGO' +AGREEMENT_FORM_TYPE_EXTERNAL_LINK = 'EXTERNAL_LINK' AGREEMENT_FORM_TYPE = ( (AGREEMENT_FORM_TYPE_STATIC, 'STATIC'), - (AGREEMENT_FORM_TYPE_DJANGO, 'DJANGO') + (AGREEMENT_FORM_TYPE_DJANGO, 'DJANGO'), + (AGREEMENT_FORM_TYPE_EXTERNAL_LINK, 'EXTERNAL LINK') ) @@ -87,18 +89,28 @@ def __str__(self): class AgreementForm(models.Model): """ This represents the type of forms that a user might need to sign to be granted access to - a data set, such as a data use agreement or rules of conduct. The form file should be an html file - that lives under static/agreementforms/. + a data set, such as a data use agreement or rules of conduct. If this is derived from an + html file, look at get_agreement_form_upload_path() to see where the file should be stored. + If this agreement form lives on an external web page, supply the URL in the external_link + field. """ name = models.CharField(max_length=100, blank=False, null=False, verbose_name="name") short_name = models.CharField(max_length=6, blank=False, null=False) + description = models.TextField(blank=True) created = models.DateTimeField(auto_now_add=True) form_file_path = models.CharField(max_length=300, blank=True, null=True) + external_link = models.CharField(max_length=300, blank=True, null=True) type = models.CharField(max_length=50, choices=AGREEMENT_FORM_TYPE, blank=True, null=True) def __str__(self): return '%s' % (self.name) + def clean(self): + if self.type == AGREEMENT_FORM_TYPE_EXTERNAL_LINK and self.form_file_path is not None: + raise ValidationError("An external link form should not have the form file path field populated.") + if self.type != AGREEMENT_FORM_TYPE_EXTERNAL_LINK and self.external_link is not None: + raise ValidationError("If the form type is not an external link, the external link field should not be populated.") + class DataProject(models.Model): """ diff --git a/app/projects/steps/dynamic_form.py b/app/projects/steps/dynamic_form.py index e2641322..9caf587a 100644 --- a/app/projects/steps/dynamic_form.py +++ b/app/projects/steps/dynamic_form.py @@ -1,5 +1,6 @@ from projects.models import SignedAgreementForm from projects.models import AGREEMENT_FORM_TYPE_STATIC +from projects.models import AGREEMENT_FORM_TYPE_EXTERNAL_LINK from projects.forms.payerdb import AccessRequestForm @@ -34,14 +35,19 @@ def agreement_form_factory(form_name, form_input=None): class SignAgreementFormsStepInitializer(ProjectStepInitializer): @staticmethod - def get_step_status(current_step, step_name, step_complete): + def get_step_status(current_step, step_name, form_type, step_complete): """ Returns the status this step should have. If the given step is incomplete and we do not already have a current_step in context, then this step is the current step and update context to note this. If this step is incomplete but another step has already been deemed - the current step, then this is a future step. + the current step, then this is a future step. Permanent steps are ones that should always + be displayed as long as all prior steps are complete. """ + # Once all prior steps are complete, display external forms permanently + if form_type == AGREEMENT_FORM_TYPE_EXTERNAL_LINK and current_step is None: + return current_step, 'permanent_step' + if step_complete: return current_step, 'completed_step' elif current_step is None: @@ -66,7 +72,7 @@ def update_context(self, project, user, current_step): ) complete = signed_forms.count() > 0 - current_step, status = self.get_step_status(current_step, form.short_name, complete) + current_step, status = self.get_step_status(current_step, form.short_name, form.type, complete) step = ProjectStep(title='Form: {name}'.format(name=form.name), project=project) @@ -75,6 +81,8 @@ def update_context(self, project, user, current_step): if not form.type or form.type == AGREEMENT_FORM_TYPE_STATIC: step.template = 'project_signup/sign_agreement_form.html' + elif form.type == AGREEMENT_FORM_TYPE_EXTERNAL_LINK: + step.template = 'project_signup/external_agreement_form.html' else: step.template = 'project_signup/dynamic_agreement_form.html' step.form = agreement_form_factory(form.form_file_path) @@ -86,4 +94,3 @@ def update_context(self, project, user, current_step): steps.append(step) return current_step, steps - diff --git a/app/projects/urls.py b/app/projects/urls.py index 6a43ccd5..a4df32db 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -10,6 +10,7 @@ from .views import signed_agreement_form from .views import save_signed_agreement_form from .views import save_dynamic_signed_agreement_form +from .views import save_signed_external_agreement_form from .views import signout from .views_manage import manage_team from .views import DataProjectView @@ -43,6 +44,7 @@ url(r'^manage_project/(?P[^/]+)/(?P[^/]+)/$', manage_project_team), url(r'^save_signed_agreement_form', save_signed_agreement_form), url(r'^save_dynamic_signed_agreement_form', save_dynamic_signed_agreement_form), + url(r'^save_signed_external_agreement_form', save_signed_external_agreement_form), url(r'^signout/$', signout), url(r'^join_team/$', join_team), url(r'^leave_team/$', leave_team), diff --git a/app/projects/views.py b/app/projects/views.py index 5e1ee897..584a7d1a 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -29,6 +29,7 @@ from .models import Participant from .models import SignedAgreementForm from .models import AGREEMENT_FORM_TYPE_STATIC +from .models import AGREEMENT_FORM_TYPE_DJANGO from .models import PERMISSION_SCHEME_EXTERNALLY_GRANTED @@ -92,6 +93,41 @@ def save_signed_agreement_form(request): return HttpResponse(200) +@user_auth_and_jwt +def save_signed_external_agreement_form(request): + """ + We cannot track if someone has signed a form on an external website, but we can at least + track that they have clicked the link to visit that website. With this record created, + an administrator can then manually verify the form on that external site and track their + approval within Hypatio. + """ + + agreement_form_id = request.POST['agreement_form_id'] + project_key = request.POST['project_key'] + + agreement_form = AgreementForm.objects.get(id=agreement_form_id) + project = DataProject.objects.get(project_key=project_key) + + # Only create a new record if one does not already exist + try: + signed_form = SignedAgreementForm.objects.get( + user=request.user, + agreement_form=agreement_form, + project=project + ) + except ObjectDoesNotExist: + agreement_text = 'The Participant accessed this form via the 3rd party website. Check there if signed appropriately.' + signed_agreement_form = SignedAgreementForm( + user=request.user, + agreement_form=agreement_form, + project=project, + date_signed=datetime.now(), + agreement_text=agreement_text + ) + signed_agreement_form.save() + + return HttpResponse(200) + @user_auth_and_jwt def submit_user_permission_request(request): @@ -123,10 +159,7 @@ def signed_agreement_form(request): if is_manager or signed_form.user == request.user: - if not signed_form.agreement_form.type or signed_form.agreement_form.type == AGREEMENT_FORM_TYPE_STATIC: - template_name = "shared/signed_agreement_form.html" - filled_out_signed_form = None - else: + if signed_form.agreement_form.type == AGREEMENT_FORM_TYPE_DJANGO: template_name = "shared/dynamic_signed_agreement_form.html" # We need to get both the type of form, and the model underlying that form, dynamically. @@ -138,6 +171,9 @@ def signed_agreement_form(request): # Populate the form with data from the model so we can render it with django bootstrap. filled_out_signed_form = form_object(instance=filled_out_form_instance) + else: + template_name = "shared/signed_agreement_form.html" + filled_out_signed_form = None return render(request, template_name, {"user": request.user, "ssl_setting": settings.SSL_SETTING, diff --git a/app/templates/base.html b/app/templates/base.html index b89235cb..f339f0ad 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -221,7 +221,7 @@ {# TODO remove the project_key eventually #} -
  • Manage Challenges
  • +
  • Manage Challenges
  • {% endif %} diff --git a/app/templates/project_signup/base.html b/app/templates/project_signup/base.html index 4f620c0b..9ada9708 100644 --- a/app/templates/project_signup/base.html +++ b/app/templates/project_signup/base.html @@ -56,6 +56,8 @@

    completed-step-header {% elif step.status == 'current_step' %} current-step-header + {% elif step.status == 'permanent_step' %} + current-step-header {% else %} blocked-step-header {% endif %}"> @@ -63,7 +65,7 @@

    - {% if step.status == 'current_step' %} + {% if step.status == 'current_step' or step.status == 'permanent_step' %}
    {% include step.template %}
    diff --git a/app/templates/project_signup/external_agreement_form.html b/app/templates/project_signup/external_agreement_form.html new file mode 100644 index 00000000..ffe5746c --- /dev/null +++ b/app/templates/project_signup/external_agreement_form.html @@ -0,0 +1,38 @@ +{% load projects_extras %} + +
    + This agreement form is hosted on an external website. Please follow the instructions below and click the button to be taken to the form. This step will remain visible until your team is activated by an administrator, but do not complete this form more than once.

    +
    + +

    + Form instructions: {{ step.agreement_form.description }} +

    + +

    + Go to {{ step.agreement_form.name }} +

    + + \ No newline at end of file From 1fec4e9856bb481ada0eb3560e241ccffb0211ae Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 28 Jun 2018 10:34:03 -0400 Subject: [PATCH 235/613] HYP-2: Removes n2c2-t1 references throughout the code base --- app/hypatio/sciauthz_services.py | 14 +++++++++++ app/profile/views.py | 4 ++-- app/projects/views.py | 19 ++++++++------- app/projects/views_files.py | 40 +++++++++++++++++--------------- app/projects/views_manage.py | 12 ++++++---- app/templates/base.html | 8 ++----- 6 files changed, 58 insertions(+), 39 deletions(-) diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index 9d7152d4..9996ba41 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -141,3 +141,17 @@ def user_has_single_permission(self, permission, value): return False else: return False + + def user_has_any_manage_permissions(self): + """ + A method used to see if this user has MANAGE permissions for any project within this application. + """ + + user_permissions = self.current_user_permissions() + + if user_permissions is not None and 'results' in user_permissions: + for permission in user_permissions["results"]: + if 'Hypatio' in permission['item'] and permission['permission'] == "MANAGE": + return True + + return False diff --git a/app/profile/views.py b/app/profile/views.py index 43c6e8b8..9b6b1e45 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -61,7 +61,7 @@ def profile(request, template_name='profile/profile.html'): user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission('n2c2-t1') + has_manage_permissions = sciauthz.user_has_any_manage_permissions() # The JWT token that will get passed in API calls jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} @@ -87,7 +87,7 @@ def profile(request, template_name='profile/profile.html'): # Generate and render the form. return render(request, template_name, {'registration_form': registration_form, 'user': user, - 'is_manager': is_manager, + 'has_manage_permissions': has_manage_permissions, 'new_user': new_user}) def get_client_ip(request): diff --git a/app/projects/views.py b/app/projects/views.py index 584a7d1a..204b66ff 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -128,6 +128,7 @@ def save_signed_external_agreement_form(request): return HttpResponse(200) + @user_auth_and_jwt def submit_user_permission_request(request): @@ -152,6 +153,7 @@ def signed_agreement_form(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) + has_manage_permissions = sciauthz.user_has_any_manage_permissions() project = get_object_or_404(DataProject, project_key=project_key) signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) @@ -177,6 +179,7 @@ def signed_agreement_form(request): return render(request, template_name, {"user": request.user, "ssl_setting": settings.SSL_SETTING, + "has_manage_permissions": has_manage_permissions, "is_manager": is_manager, "signed_form": signed_form, "filled_out_signed_form": filled_out_signed_form, @@ -194,7 +197,7 @@ def list_data_projects(request, template_name='dataprojects/list.html'): projects_with_view_permissions = [] projects_with_access_requests = {} - is_manager = False + has_manage_permissions = False if not request.user.is_authenticated(): user = None @@ -207,7 +210,7 @@ def list_data_projects(request, template_name='dataprojects/list.html'): return logout_redirect(request) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission('n2c2-t1') + has_manage_permissions = sciauthz.user_has_any_manage_permissions() user_permissions = sciauthz.current_user_permissions() user_access_requests = sciauthz.current_user_access_requests() @@ -269,7 +272,7 @@ def list_data_projects(request, template_name='dataprojects/list.html'): return render(request, template_name, {"data_projects": data_projects, "user": user, "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) @@ -280,7 +283,7 @@ def list_data_challenges(request, template_name='datacontests/list.html'): all_data_contests = DataProject.objects.filter(is_contest=True, visible=True) data_contests = [] - is_manager = False + has_manage_permissions = False if not request.user.is_authenticated(): user = None @@ -297,7 +300,7 @@ def list_data_challenges(request, template_name='datacontests/list.html'): all_data_contests = DataProject.objects.filter(is_contest=True) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) - is_manager = sciauthz.user_has_manage_permission('n2c2-t1') + has_manage_permissions = sciauthz.user_has_any_manage_permissions() # Build the dictionary with all project and permission information needed for data_contest in all_data_contests: @@ -315,7 +318,7 @@ def list_data_challenges(request, template_name='datacontests/list.html'): return render(request, template_name, {"data_contests": data_contests, "user": user, "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "account_server_url": settings.ACCOUNT_SERVER_URL, "profile_server_url": settings.SCIREG_SERVER_URL}) @@ -396,7 +399,7 @@ def get_base_context_data(self, context): # Check if the user is a manager of this DataProject. if self.request.user.is_authenticated(): sciauthz = SciAuthZ(settings.AUTHZ_BASE, self.user_jwt, self.request.user.email) - context['is_manager'] = sciauthz.user_has_manage_permission(self.project.project_key) + context['has_manage_permissions'] = sciauthz.user_has_manage_permission(self.project.project_key) context['has_view_permission'] = sciauthz.user_has_single_permission(self.project.project_key, "VIEW") def get_unregistered_context(self, context): @@ -802,7 +805,7 @@ def is_user_granted_access(self, context): """ # Does user have VIEW or MANAGE permissions? - if not context['has_view_permission'] and not context['is_manager']: + if not context['has_view_permission'] and not context['has_manage_permissions']: return False # If the permission is managed outside this project, return false diff --git a/app/projects/views_files.py b/app/projects/views_files.py index 6233e084..e0e82de3 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -48,17 +48,18 @@ def download_dataset(request): logger.debug("[views_files][download_dataset] - Attempting file download.") + file_id = request.GET.get("file_id") + file_to_download = get_object_or_404(HostedFile, id=file_id) + project_key = file_to_download.project.project_key + # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) - if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): + if not sciauthz.user_has_single_permission(project_key, "VIEW"): logger.debug("[views_files][download_dataset] - No Access for user " + request.user.email) return HttpResponse("You do not have access to download this file.", status=403) - file_id = request.GET.get("file_id") - file_to_download = get_object_or_404(HostedFile, id=file_id) - # Save a record of this person downloading this file HostedFileDownload.objects.create(user=request.user, hosted_file=file_to_download) @@ -206,21 +207,22 @@ def upload_participantsubmission_file(request): if request.method == 'POST': logger.debug('post') - # Check that user has permissions to be submitting files for this project. - user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) - - if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): - logger.debug("[views_files][upload_participantsubmission_file] - No Access for user " + request.user.email) - return HttpResponse("You do not have access to upload this file.", status=403) - # Assembles the form and runs validation. filename = request.POST.get('filename') project = request.POST.get('project') + if not filename or not project: logger.error('No filename or no project!') return HttpResponse('Filename and project are required', status=400) + # Check that user has permissions to be submitting files for this project. + user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + + if not sciauthz.user_has_single_permission(project, "VIEW"): + logger.debug("[views_files][upload_participantsubmission_file] - No Access for user " + request.user.email) + return HttpResponse("You do not have access to upload this file.", status=403) + if filename.split(".")[-1] != "zip": logger.error('Not a zip file.') return HttpResponse("Only .zip files are accepted", status=400) @@ -338,19 +340,19 @@ def delete_participantsubmission(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) - if not sciauthz.user_has_single_permission("n2c2-t1", "VIEW"): - logger.debug( - "[views_files][delete_participantsubmission] - No Access for user %s", - request.user.email - ) - return HttpResponse("You do not have access to delete this file.", status=403) - submission_uuid = request.POST.get('submission_uuid') submission = ParticipantSubmission.objects.get(uuid=submission_uuid) project = submission.participant.data_challenge team = submission.participant.team + if not sciauthz.user_has_single_permission(project.project_key, "VIEW"): + logger.debug( + "[views_files][delete_participantsubmission] - No Access for user %s", + request.user.email + ) + return HttpResponse("You do not have access to delete this file.", status=403) + logger.debug( '[views_files][delete_participantsubmission] - %s is trying to delete submission %s', request.user.email, diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index c5257797..1a1f4f6d 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -33,6 +33,7 @@ def manage_project(request, project_key, template_name='manage/manageprojects.ht sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) + has_manage_permissions = sciauthz.user_has_any_manage_permissions() project = DataProject.objects.get(project_key=project_key) @@ -44,7 +45,7 @@ def manage_project(request, project_key, template_name='manage/manageprojects.ht teams = Team.objects.filter(data_project=project) return render(request, template_name, context={"user": user, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "project": project, "teams": teams, }) @@ -101,6 +102,7 @@ def manage_project_team(request, project_key, team_leader, template_name='manage sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) + has_manage_permissions = sciauthz.user_has_any_manage_permissions() if not is_manager: logger.debug( @@ -126,7 +128,7 @@ def manage_project_team(request, project_key, team_leader, template_name='manage return render(request, template_name, context={"user": user, "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "project": project, "team": team, "team_members": team_member_details, @@ -147,6 +149,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) + has_manage_permissions = sciauthz.user_has_any_manage_permissions() if not is_manager: logger.debug( @@ -214,7 +217,7 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m return render(request, template_name, context={"user": user, "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "project": project, "team": team, "team_members": team_member_details, @@ -238,6 +241,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) + has_manage_permissions = sciauthz.user_has_any_manage_permissions() if not is_manager: logger.debug( @@ -289,7 +293,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte return render(request, template_name, {"user": user, "ssl_setting": settings.SSL_SETTING, - "is_manager": is_manager, + "has_manage_permissions": has_manage_permissions, "project": project, "teams": teams, "users_without_a_team_details": users_without_a_team_details, diff --git a/app/templates/base.html b/app/templates/base.html index f339f0ad..c8c93fcb 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -215,13 +215,9 @@ + + {% endblock %} From 68218e63cf26db6f4afe738da73397e0ebebc7ce Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 5 Jul 2018 14:48:30 -0400 Subject: [PATCH 243/613] HYP-51: Passes the project onto SciAuthZ when creating profile permissions --- app/hypatio/sciauthz_services.py | 15 +++++++++++++-- app/projects/views_teams.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index 9996ba41..6f3d9cc0 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -83,13 +83,24 @@ def current_user_request_access(self, access_request): return user_access_request - def create_profile_permission(self, grantee_email): + def create_profile_permission(self, grantee_email, project): logger.debug('[HYPATIO][create_profile_permission] - Creating Profile Permissions') modified_headers = self.JWT_HEADERS modified_headers['Content-Type'] = 'application/x-www-form-urlencoded' - profile_permission = requests.post(self.CREATE_PROFILE_PERMISSION, headers=modified_headers, data={"grantee_email": grantee_email}, verify=settings.VERIFY_REQUESTS) + data = { + "grantee_email": grantee_email, + "item": 'Hypatio.' + project + } + + profile_permission = requests.post( + self.CREATE_PROFILE_PERMISSION, + headers=modified_headers, + data=data, + verify=settings.VERIFY_REQUESTS + ) + return profile_permission def create_view_permission(self, project, grantee_email): diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index e372f389..7ce4a1f2 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -379,7 +379,7 @@ def join_team(request): # Create record to allow leader access to profile. sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) - sciauthz.create_profile_permission(team_leader) + sciauthz.create_profile_permission(team_leader, project_key) return redirect('/projects/' + request.POST.get('project_key') + '/') From 3bb604cb5661be5b19b533f6d2fdafe1c6c31da7 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 11 Jul 2018 11:51:37 -0400 Subject: [PATCH 244/613] HYP-53: Fixes error when joining a team that existed in a previous challenge join_team function was not factoring in project when searching for teams or participants. --- app/projects/views_teams.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 7ce4a1f2..493413de 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -336,13 +336,13 @@ def join_team(request): team_leader = request.POST.get("team_leader") try: - participant = Participant.objects.get(user=request.user) + participant = Participant.objects.get(user=request.user, data_challenge=project) except ObjectDoesNotExist: participant = create_participant(request.user, project) try: # If this team leader has already created a team, add the person to the team in a pending status - team = Team.objects.get(team_leader__email__iexact=team_leader) + team = Team.objects.get(team_leader__email__iexact=team_leader, data_project=project) # Only allow a new participant to join a team that is still in a pending or ready state if team.status in ['Pending', 'Ready']: From 6b5a02f3f4af61209bab6757b283052be8ec3914 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 11 Jul 2018 14:14:10 -0400 Subject: [PATCH 245/613] HYP-52: Fixes bug where viewing a signed agreement form expects a participant record Participant record should not need to exist for you to view their agreement form --- app/projects/views.py | 6 +++++- app/projects/views_teams.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/projects/views.py b/app/projects/views.py index 1080bad5..012832e2 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -158,7 +158,11 @@ def signed_agreement_form(request): project = get_object_or_404(DataProject, project_key=project_key) signed_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id, project=project) - participant = Participant.objects.get(data_challenge=project, user=signed_form.user) + + try: + participant = Participant.objects.get(data_challenge=project, user=signed_form.user) + except ObjectDoesNotExist: + participant = None if is_manager or signed_form.user == request.user: diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 493413de..83d7456a 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -79,11 +79,16 @@ def change_signed_form_status(request): email_template='email_signed_form_rejection_notification', extra=context) - participant = Participant.objects.get(user=affected_user, data_challenge=signed_form.project) - team = participant.team + # If the user is a participant on a team, then the team status may need to be changed + try: + participant = Participant.objects.get(user=affected_user, data_challenge=signed_form.project) + team = participant.team + except ObjectDoesNotExist: + participant = None + team = None # If the team is in an Active status, move the team status down to Ready and remove everyone's VIEW permissions - if team.status == "Active": + if team is not None and team.status == "Active": team.status = "Ready" team.save() From c63dbdecc1fb8e2e4aab70e6e2aa22300e73514e Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 11 Jul 2018 14:38:31 -0400 Subject: [PATCH 246/613] HYP-55: Fixes bug where participants without a team shows blank buttons for agreement forms not yet signed --- app/projects/views_manage.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index c228bada..d6cd611e 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -304,8 +304,15 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte # For each of the available agreement forms for this project, display only latest version completed by the user for agreement_form in project.agreement_forms.all(): - signed_agreement_forms.append(SignedAgreementForm.objects.filter(user__email=email, project=project, - agreement_form=agreement_form).last()) + + signed_form = SignedAgreementForm.objects.filter( + user__email=email, + project=project, + agreement_form=agreement_form + ).last() + + if signed_form: + signed_agreement_forms.append(signed_form) users_without_a_team_details.append({ 'email': email, From 93278403c1d6f6cf66c68f5170052449819bb9de Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 13 Jul 2018 09:56:18 -0400 Subject: [PATCH 247/613] HYP-49: Adds support for multiple project supervisors All contact form messages will now get sent to all supervisors. --- app/contact/views.py | 4 ++-- app/projects/admin.py | 2 +- app/projects/fixtures/projects.json | 14 +++++------ .../migrations/0050_auto_20180713_1346.py | 24 +++++++++++++++++++ app/projects/models/models.py | 3 ++- 5 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 app/projects/migrations/0050_auto_20180713_1346.py diff --git a/app/contact/views.py b/app/contact/views.py index 78a4f205..0d946eb6 100644 --- a/app/contact/views.py +++ b/app/contact/views.py @@ -42,8 +42,8 @@ def contact_form(request): try: project = DataProject.objects.get(project_key=form.cleaned_data['project']) - if project.project_supervisor != '': - recipients = [project.project_supervisor] + if project.project_supervisors != '': + recipients = project.project_supervisors.split(',') else: recipients = settings.CONTACT_FORM_RECIPIENTS.split(',') diff --git a/app/projects/admin.py b/app/projects/admin.py index a7308f31..192b3ba9 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -13,7 +13,7 @@ from .models import PayerDBForm class DataprojectAdmin(admin.ModelAdmin): - list_display = ('name', 'project_key', 'is_contest', 'project_supervisor') + list_display = ('name', 'project_key', 'is_contest') class AgreementformAdmin(admin.ModelAdmin): list_display = ('name', 'short_name', 'type', 'form_file_path') diff --git a/app/projects/fixtures/projects.json b/app/projects/fixtures/projects.json index 78dca48d..80672ad3 100644 --- a/app/projects/fixtures/projects.json +++ b/app/projects/fixtures/projects.json @@ -49,7 +49,7 @@ "project_key": "n2c2-t1", "permission_scheme": "PRIVATE", "agreement_forms": [1, 2], - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": true, "visible": true, "submission_form_file_path": "submissionforms/n2c2-t1.html" @@ -65,7 +65,7 @@ "short_description": "Three Clinical Trials of Nutritional Supplements for Retinitis Pigmentosa", "project_key": "BERSON", "permission_scheme": "PRIVATE", - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } @@ -107,7 +107,7 @@ "short_description": "A database of human exposomes and phenomes from the US National Health and Nutrition Examination Survey", "project_key": "NHANES", "permission_scheme": "PUBLIC", - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } @@ -122,7 +122,7 @@ "short_description": "NIH/NCATS Global Rare Diseases Patient Registry i2b2/tranSMART Data Repository", "project_key": "GRDR", "permission_scheme": "PUBLIC", - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } @@ -137,7 +137,7 @@ "short_description": "Simons Simplex Collection", "project_key": "SSC", "permission_scheme": "PUBLIC", - "project_supervisor": "nathaniel_bessa@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } @@ -152,7 +152,7 @@ "short_description": "Exposome Data Warehouse", "project_key": "EDW", "permission_scheme": "PUBLIC", - "project_supervisor": "michael_mcduffie@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } @@ -167,7 +167,7 @@ "short_description": "Payer Database", "project_key": "PAYERDB", "permission_scheme": "PUBLIC", - "project_supervisor": "michael_mcduffie@hms.harvard.edu", + "project_supervisors": "nathaniel_bessa@hms.harvard.edu", "is_contest": false, "visible": true } diff --git a/app/projects/migrations/0050_auto_20180713_1346.py b/app/projects/migrations/0050_auto_20180713_1346.py new file mode 100644 index 00000000..654d3613 --- /dev/null +++ b/app/projects/migrations/0050_auto_20180713_1346.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-07-13 13:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0049_agreementform_description'), + ] + + operations = [ + migrations.RemoveField( + model_name='dataproject', + name='project_supervisor', + ), + migrations.AddField( + model_name='dataproject', + name='project_supervisors', + field=models.CharField(blank=True, max_length=1024, null=True, verbose_name='Project Supervisors (comma-delimited, no spaces)'), + ), + ] diff --git a/app/projects/models/models.py b/app/projects/models/models.py index 48a70465..d1cccf20 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -119,6 +119,7 @@ class DataProject(models.Model): This represents a data project that users can access, along with its permissions and requirements. A DataProject can be simply a data set or it can be a data contest as recognized by the is_contest flag. The submission form file should be an html file that lives under static/submissionforms/. + Project_supervisors should be a comma delimited string of email addresses. """ name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Name of project", unique=False) @@ -126,7 +127,7 @@ class DataProject(models.Model): institution = models.ForeignKey(Institution, blank=True, null=True, on_delete=models.PROTECT) description = models.TextField(blank=True, null=True, verbose_name="Description") short_description = models.CharField(max_length=255, blank=True, null=True, verbose_name="Short Description") - project_supervisor = models.CharField(max_length=255, blank=True, null=True, verbose_name="Project Supervisor") + project_supervisors = models.CharField(max_length=1024, blank=True, null=True, verbose_name="Project Supervisors (comma-delimited, no spaces)") visible = models.BooleanField(default=False, blank=False, null=False) permission_scheme = models.CharField(max_length=100, default="PRIVATE", verbose_name="Permission Scheme") From ff5552d781330dbc9f4dbfb8b7cae7cc1db938ee Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 13 Jul 2018 10:24:51 -0400 Subject: [PATCH 248/613] HYP-49: Empty project_supervisors field is None not empty string --- app/contact/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/contact/views.py b/app/contact/views.py index 0d946eb6..4580ffd8 100644 --- a/app/contact/views.py +++ b/app/contact/views.py @@ -42,7 +42,7 @@ def contact_form(request): try: project = DataProject.objects.get(project_key=form.cleaned_data['project']) - if project.project_supervisors != '': + if project.project_supervisors != '' and project.project_supervisors is not None: recipients = project.project_supervisors.split(',') else: recipients = settings.CONTACT_FORM_RECIPIENTS.split(',') From 9c46c814c6685ca104637dbdfe6cea9434a4ffa8 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 09:57:34 -0400 Subject: [PATCH 249/613] HYP-56: Changes to participant email list download button A key stakeholder wanted the email list download button to have specific filters that might not be relevant for future projects. So I abstracted the download_email_list_of_ready_participants function into something more flexible. It now accepts a few different parameters when determining a list of emails to download. Lastly, it adds first and last name to the download. A TODO note is added this could be problematic in prod which has many more users. Querying SciReg for each user's first/last name could cause timeouts because SciReg will need to check SciAuthZ for each call. --- app/projects/models/models.py | 23 ++++--- app/projects/urls.py | 4 +- app/projects/views_manage.py | 60 +++++++++++++++---- .../datacontests/managecontests.html | 10 +++- 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/app/projects/models/models.py b/app/projects/models/models.py index d1cccf20..2e90a162 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -16,18 +16,27 @@ ) +TEAM_PENDING = 'Pending' +TEAM_READY = 'Ready' +TEAM_ACTIVE = 'Active' +TEAM_DEACTIVATED = 'Deactivated' + TEAM_STATUS = ( - ('Pending', 'Pending'), - ('Ready', 'Ready to be activated'), - ('Active', 'Activated'), - ('Deactivated', 'Deactivated') + (TEAM_PENDING, 'Pending'), + (TEAM_READY, 'Ready to be activated'), + (TEAM_ACTIVE, 'Activated'), + (TEAM_DEACTIVATED, 'Deactivated') ) +SIGNED_FORM_PENDING_APPROVAL = 'P' +SIGNED_FORM_APPROVED = 'A' +SIGNED_FORM_REJECTED = 'R' + SIGNED_FORM_STATUSES = ( - ('P', 'Pending Approval'), - ('A', 'Approved'), - ('R', 'Rejected'), + (SIGNED_FORM_PENDING_APPROVAL, 'Pending Approval'), + (SIGNED_FORM_APPROVED, 'Approved'), + (SIGNED_FORM_REJECTED, 'Rejected'), ) AGREEMENT_FORM_TYPE_STATIC = 'STATIC' diff --git a/app/projects/urls.py b/app/projects/urls.py index f79f946e..fba51f8f 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -15,7 +15,7 @@ from .views_manage import manage_contest from .views_manage import manage_project from .views_manage import manage_project_team -from .views_manage import download_email_list_of_ready_participants +from .views_manage import download_email_list from .views_teams import join_team from .views_teams import leave_team @@ -64,6 +64,6 @@ url(r'^upload_participantsubmission_file/$', upload_participantsubmission_file), url(r'^download_team_submissions/$', download_team_submissions), url(r'^delete_participantsubmission/$', delete_participantsubmission), - url(r'^download_email_list_of_ready_participants/$', download_email_list_of_ready_participants), + url(r'^download_email_list/$', download_email_list), url(r'^(?P[^/]+)/$', DataProjectView.as_view()), ) diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index d6cd611e..cc46885b 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -13,6 +13,7 @@ from .models import DataProject from .models import Team from .models import TeamComment +from .models import AgreementForm from .models import SignedAgreementForm from .models import HostedFile from .models import HostedFileDownload @@ -231,13 +232,14 @@ def manage_team(request, project_key, team_leader, template_name='datacontests/m @user_auth_and_jwt -def download_email_list_of_ready_participants(request): +def download_email_list(request): """ - Downloads a text file containing the email addresses of all participants on teams - marked as ready to activate. + Downloads a text file containing the email addresses of participants of a given project + with filters accepted as GET parameters. Accepted filters include: team, team status, + agreement form ID, and agreement form status. """ - logger.debug("[views_manage][get_all_participant_emails] - Attempting file download.") + logger.debug("[views_manage][download_email_list] - Attempting file download.") project_key = request.GET.get("project") project = get_object_or_404(DataProject, project_key=project_key) @@ -248,17 +250,55 @@ def download_email_list_of_ready_participants(request): is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: - logger.debug("[views_manage][get_all_participant_emails] - No Access for user " + request.user.email) + logger.debug("[views_manage][download_email_list] - No Access for user " + request.user.email) return HttpResponse("You do not have access to download this file.", status=403) - # Find all the participans on teams marked Ready to Activate - ready_teams = Team.objects.filter(data_project=project, status='Ready') - ready_participants = Participant.objects.filter(team__in=ready_teams) + # Filters used to determine the list of participants + filter_team = request.GET.get("team-id", None) + filter_team_status = request.GET.get("team-status", None) + filter_signed_agreement_form = request.GET.get("agreement-form-id", None) + filter_signed_agreement_form_status = request.GET.get("agreement-form-status", None) + + # Apply filters that narrow the scope of teams + teams = Team.objects.filter(data_project=project) + + if filter_team: + teams = teams.filter(id=filter_team) + if filter_team_status: + teams = teams.filter(status=filter_team_status) + + # Apply filters that narrow the list of participants + participants = Participant.objects.filter(team__in=teams) + + if filter_signed_agreement_form: + agreement_form = AgreementForm.objects.get(id=filter_signed_agreement_form) + + # Find all signed instances of this form + signed_forms = SignedAgreementForm.objects.filter(agreement_form=agreement_form) + if filter_signed_agreement_form_status: + signed_forms = signed_forms.filter(status=filter_signed_agreement_form_status) + + # Narrow down the participant list with just those who have these signed forms + signed_forms_users = signed_forms.values_list('user', flat=True) + participants = participants.filter(user__in=signed_forms_users) # Build a string that will be the contents of the file file_contents = "" - for participant in ready_participants: - file_contents += participant.user.email + "\n" + for participant in participants: + + # TODO MIGHT NEED TO MAKE A SCIREG METHOD TO PASS ALL EMAILS AT ONCE + # OTHERWISE SCIREG IS MAKING INDIVIDUAL CALLS TO SCIAUTHZ FOR EACH. + + # Get the person's first and last name + try: + profile = get_user_profile(user_jwt, participant.user.email, project_key) + first_name = profile["results"][0]['first_name'] + last_name = profile["results"][0]['last_name'] + except (KeyError, IndexError): + first_name = "" + last_name = "" + + file_contents += participant.user.email + " " + first_name + " " + last_name + "\n" response = HttpResponse(file_contents, content_type='text/plain') response['Content-Disposition'] = 'attachment; filename="%s"' % 'pending_participants.txt' diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index dac8d547..a71b8aa7 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -139,9 +139,13 @@

    Challenge Statistics

    Downloads

    - - Participant Emails on Ready Teams - + {% for form in project.agreement_forms.all %} + {% if form.type == "EXTERNAL_LINK" %} + + {{ form.name }} Forms to Approve + + {% endif %} + {% endfor %}
    From c4b862edbae97132d2d52fb4fedbadee106a6eb4 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 10:10:37 -0400 Subject: [PATCH 250/613] HYP-57: Fixes >1 Participant record found bug and adds logging --- app/projects/views_teams.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 83d7456a..9acd6c1c 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -318,12 +318,21 @@ def reject_team_join(request): @user_auth_and_jwt def leave_team(request): - project_key = request.POST.get("project_key") - project = DataProject.objects.get(project_key=project_key) + """ + Removes the participant from whatever team they are currently on or have requested to be on. + """ - team_leader = request.POST.get("team_leader") + project_key = request.POST.get("project_key", "") + + logger.debug("[HYPATIO][leave_team] User " + request.user.email + " trying to leave their current team for project " + project_key + ".") + + try: + project = DataProject.objects.get(project_key=project_key) + except ObjectDoesNotExist: + logger.error("[HYPATIO][leave_team] DataProject not found for given project_key: " + project_key) + return HttpResponse(500) - participant = Participant.objects.get(user=request.user) + participant = Participant.objects.get(user=request.user, data_challenge=project) participant.team = None participant.pending = False participant.approved = False From 2ebd8b9eaa818d3ded5cd6a9b51ee624e422df58 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 11:13:20 -0400 Subject: [PATCH 251/613] HYP-56: UI cleanup --- app/templates/datacontests/managecontests.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/templates/datacontests/managecontests.html b/app/templates/datacontests/managecontests.html index a71b8aa7..a087790c 100644 --- a/app/templates/datacontests/managecontests.html +++ b/app/templates/datacontests/managecontests.html @@ -136,16 +136,17 @@

    Challenge Statistics

    - {% for form in project.agreement_forms.all %} - {% if form.type == "EXTERNAL_LINK" %} - - {{ form.name }} Forms to Approve - - {% endif %} - {% endfor %} +

    Please only click once and wait for the download to complete. This may take a few moments.

    + {% for form in project.agreement_forms.all %} + {% if form.type == "EXTERNAL_LINK" %} +

    + {{ form.name }} Forms to Approve +

    + {% endif %} + {% endfor %}
    From d3c9fd5e85e622dc974fd9dd652548c590886b41 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 12:03:15 -0400 Subject: [PATCH 252/613] HYP-56: Comment out SciReg calls until a more efficient endpoint is ready Timeouts in production --- app/projects/views_manage.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index cc46885b..609dea44 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -289,14 +289,16 @@ def download_email_list(request): # TODO MIGHT NEED TO MAKE A SCIREG METHOD TO PASS ALL EMAILS AT ONCE # OTHERWISE SCIREG IS MAKING INDIVIDUAL CALLS TO SCIAUTHZ FOR EACH. + first_name = "" + last_name = "" + # Get the person's first and last name - try: - profile = get_user_profile(user_jwt, participant.user.email, project_key) - first_name = profile["results"][0]['first_name'] - last_name = profile["results"][0]['last_name'] - except (KeyError, IndexError): - first_name = "" - last_name = "" + # try: + # profile = get_user_profile(user_jwt, participant.user.email, project_key) + # first_name = profile["results"][0]['first_name'] + # last_name = profile["results"][0]['last_name'] + # except (KeyError, IndexError): + # pass file_contents += participant.user.email + " " + first_name + " " + last_name + "\n" From 2eea153e74d6a2fedf7e9b4c984f19910ed077a7 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 14:33:39 -0400 Subject: [PATCH 253/613] HYP-56: Use new SciReg endpoint to add names to email list download --- app/hypatio/scireg_services.py | 34 ++++++++++++++++++++++++++++++---- app/projects/views_manage.py | 22 ++++++++++++---------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index bae96019..128aa3c5 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -61,12 +61,12 @@ def get_current_user_profile(user_jwt): return profile -def get_user_profile(user_jwt, email_of_profile, project): +def get_user_profile(user_jwt, email_of_profile, project_key): f = furl(settings.SCIREG_REGISTRATION_URL) f.args["email"] = email_of_profile - f.args["project"] = 'Hypatio.' + project + f.args["project"] = 'Hypatio.' + project_key try: profile = requests.get(f.url, headers=build_headers_with_jwt(user_jwt)).json() @@ -76,7 +76,7 @@ def get_user_profile(user_jwt, email_of_profile, project): return profile -def get_distinct_countries_participating(user_jwt, participants, project): +def get_distinct_countries_participating(user_jwt, participants, project_key): """ Takes a QuerySet of participants' emails and returns a dictionary containing the unique countries of these participants and a count for each. @@ -90,7 +90,7 @@ def get_distinct_countries_participating(user_jwt, participants, project): data = { 'emails': emails_list_string, - 'project': 'Hypatio.' + project, + 'project': 'Hypatio.' + project_key, } try: @@ -100,3 +100,29 @@ def get_distinct_countries_participating(user_jwt, participants, project): return None return countries + + +def get_names(user_jwt, participants, project_key): + """ + Takes a QuerySet of participants' emails and returns a dictionary + containing the first and last names of each participant. + """ + + url = settings.SCIREG_REGISTRATION_URL + 'get_names/' + + # From a QuerySet of participants, get a list of their emails + emails = list(participants.values_list('user__email', flat=True)) + emails_list_string = ",".join(emails) + + data = { + 'emails': emails_list_string, + 'project': 'Hypatio.' + project_key, + } + + try: + names = requests.post(url, headers=build_headers_with_jwt(user_jwt), data=json.dumps(data)).json() + except Exception: + logger.error('Failed to get names of participants from SciReg.') + return None + + return names diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index 609dea44..0b904ecc 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -1,4 +1,5 @@ import logging +import json from django.conf import settings from django.contrib.auth.models import User @@ -22,6 +23,7 @@ from hypatio.scireg_services import get_distinct_countries_participating from hypatio.scireg_services import get_user_profile +from hypatio.scireg_services import get_names # Get an instance of a logger logger = logging.getLogger(__name__) @@ -282,23 +284,23 @@ def download_email_list(request): signed_forms_users = signed_forms.values_list('user', flat=True) participants = participants.filter(user__in=signed_forms_users) + # Query SciReg to get a dictionary of first and last names for each participant + names = json.loads(get_names(user_jwt, participants, project_key)) + # Build a string that will be the contents of the file file_contents = "" for participant in participants: - # TODO MIGHT NEED TO MAKE A SCIREG METHOD TO PASS ALL EMAILS AT ONCE - # OTHERWISE SCIREG IS MAKING INDIVIDUAL CALLS TO SCIAUTHZ FOR EACH. - first_name = "" last_name = "" - # Get the person's first and last name - # try: - # profile = get_user_profile(user_jwt, participant.user.email, project_key) - # first_name = profile["results"][0]['first_name'] - # last_name = profile["results"][0]['last_name'] - # except (KeyError, IndexError): - # pass + # Look in our dictionary of names from SciReg for this participant + try: + name = names[participant.user.email] + first_name = name['first_name'] + last_name = name['last_name'] + except (KeyError, IndexError): + pass file_contents += participant.user.email + " " + first_name + " " + last_name + "\n" From 02dd3d601ec9e675d35af8dafc756cba4569545f Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Wed, 18 Jul 2018 16:23:52 -0400 Subject: [PATCH 254/613] HYP-30: Check for results element in SciAuthZ payload Should prevent the `KeyError: 'count' in Hypatio` error --- app/hypatio/sciauthz_services.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index 6f3d9cc0..095b0c59 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -140,18 +140,15 @@ def user_has_single_permission(self, permission, value): user_permissions = requests.get(f.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() except JSONDecodeError: logger.debug("[SCIAUTHZ][user_has_single_permission] - No Valid permissions returned.") - user_permissions = {"count": 0} - - if user_permissions["count"] > 0: + return False + if user_permissions is not None and 'results' in user_permissions: # A user may have multiple permissions, check if one of them is the one we're looking for for permission in user_permissions["results"]: if permission["permission"] == value: return True - return False - else: - return False + return False def user_has_any_manage_permissions(self): """ From e6d7bd735be6ef015018e8613e3e90f66fcb2634 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 19 Jul 2018 14:38:35 -0400 Subject: [PATCH 255/613] HYP-58: Enable logging forward to Sentry --- app/hypatio/settings.py | 15 +++++++++++++++ gunicorn-nginx-entry.sh | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 745e9bba..2c8c9c96 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -235,6 +235,11 @@ LOGGING = { 'version': 1, 'handlers': { + 'sentry': { + 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'service': 'HYPATIO'}, + }, 'console': { 'class': 'logging.StreamHandler', 'stream': sys.stdout, @@ -260,6 +265,16 @@ 'level': 'ERROR', 'propagate': True, }, + 'raven': { + 'level': 'WARNING', + 'handlers': ['console'], + 'propagate': False, + }, + 'sentry.errors': { + 'level': 'WARNING', + 'handlers': ['console'], + 'propagate': False, + }, }, } diff --git a/gunicorn-nginx-entry.sh b/gunicorn-nginx-entry.sh index f6cd29c4..4af8d5ab 100644 --- a/gunicorn-nginx-entry.sh +++ b/gunicorn-nginx-entry.sh @@ -20,6 +20,8 @@ EMAIL_HOST_USER=$(aws ssm get-parameters --names $PS_PATH.email_host_user --with EMAIL_HOST_PASSWORD=$(aws ssm get-parameters --names $PS_PATH.email_host_password --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') EMAIL_PORT=$(aws ssm get-parameters --names $PS_PATH.email_port --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') +RAVEN_URL=$(aws ssm get-parameters --names $PS_PATH.raven_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') + export COOKIE_DOMAIN export SITE_URL=$SITE_URL export ALLOWED_HOSTS=$ALLOWED_HOSTS @@ -40,6 +42,8 @@ export EMAIL_HOST_USER export EMAIL_HOST_PASSWORD export EMAIL_PORT +export RAVEN_URL + ACCOUNT_SERVER_URL=$(aws ssm get-parameters --names $PS_PATH.authentication_login_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') SCIREG_SERVER_URL=$(aws ssm get-parameters --names $PS_PATH.register_user_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') AUTHZ_BASE=$(aws ssm get-parameters --names $PS_PATH.authorization_server_url --with-decryption --region us-east-1 | jq -r '.Parameters[].Value') From c54dc9638ec05ea0517e1df77a1f5928f969f32d Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 20 Jul 2018 11:31:18 -0400 Subject: [PATCH 256/613] HYP-62: Updates Raven to show HYPATIO as the name of the app So we can distinguish Senty alerts within the SCI buckets on Sentry. --- app/hypatio/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 2c8c9c96..7fcee5f3 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -236,9 +236,8 @@ 'version': 1, 'handlers': { 'sentry': { - 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. + 'level': 'ERROR', 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', - 'tags': {'service': 'HYPATIO'}, }, 'console': { 'class': 'logging.StreamHandler', @@ -283,6 +282,7 @@ # If you are using git, you can also automatically configure the # release based on the git info. 'release': '1', + 'name': 'HYPATIO' } try: From 7797484800c7e6bc093a1a9c6ca7951c6a9606a5 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 20 Jul 2018 11:44:50 -0400 Subject: [PATCH 257/613] HYP-62: Change Raven Config from NAME to SITE Name == server_name which is not technically "hypatio" --- app/hypatio/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index 7fcee5f3..b90c3e5d 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -282,7 +282,7 @@ # If you are using git, you can also automatically configure the # release based on the git info. 'release': '1', - 'name': 'HYPATIO' + 'site': 'HYPATIO' } try: From ec16769a27ff8379eaf39a68e5d55a81f670f63e Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 20 Jul 2018 13:54:32 -0400 Subject: [PATCH 258/613] HYP-60: Resolves NoneType errors when user is logged in but missing JWT cookie Latest version of pyauth0jwt will log the user out in this case. --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index 0f539b6e..04d5d31b 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,7 +11,7 @@ furl==1.0.1 mock==2.0.0 mysqlclient==1.3.9 Pillow==5.0.0 -py-auth0-jwt==0.2.14 +py-auth0-jwt==0.3.0 py-auth0-jwt-rest==0.1.2 python-pstore==0.8 PyJWT==1.6.1 From 67be1cdf588cc1aed5b88460bbcc6d8f130e368d Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 20 Jul 2018 15:58:26 -0400 Subject: [PATCH 259/613] HYP-18: Improve some of the link names --- app/hypatio/urls.py | 12 +++++++++--- app/projects/urls.py | 3 --- app/templates/base.html | 4 ++-- app/templates/dataprojects/list.html | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/hypatio/urls.py b/app/hypatio/urls.py index b7ca02ce..f6e0d9c8 100644 --- a/app/hypatio/urls.py +++ b/app/hypatio/urls.py @@ -1,13 +1,19 @@ -from django.conf.urls import url, include -from django.contrib import admin -from django.views.defaults import page_not_found from django.conf import settings +from django.conf.urls import include +from django.conf.urls import url from django.conf.urls.static import static +from django.contrib import admin +from django.views.defaults import page_not_found + +from projects.views import list_data_projects +from projects.views import list_data_challenges urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^contact/', include('contact.urls')), url(r'^projects/', include('projects.urls')), url(r'^profile/', include('profile.urls')), + url(r'^data-sets/$', list_data_projects), + url(r'^data-challenges/$', list_data_challenges), url(r'^', include('projects.urls')), ] \ No newline at end of file diff --git a/app/projects/urls.py b/app/projects/urls.py index fba51f8f..1b37b3bb 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -1,7 +1,6 @@ from django.conf.urls import url from .views import list_data_projects -from .views import list_data_challenges from .views import request_access from .views import submit_user_permission_request from .views import signed_agreement_form @@ -36,8 +35,6 @@ urlpatterns = ( url(r'^$', list_data_projects), - url(r'^list_data_projects/$', list_data_projects), - url(r'^list_data_challenges/$', list_data_challenges), url(r'^request_access/$', request_access), url(r'^submit_user_permission_request/$', submit_user_permission_request), url(r'^manage/(?P[^/]+)/$', manage_contest, name='manage_contest'), diff --git a/app/templates/base.html b/app/templates/base.html index c8c93fcb..a64e4764 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -197,8 +197,8 @@ Project Type diff --git a/app/templates/dataprojects/list.html b/app/templates/dataprojects/list.html index 8060aa39..caa66e64 100644 --- a/app/templates/dataprojects/list.html +++ b/app/templates/dataprojects/list.html @@ -14,8 +14,8 @@ {% endblock %} -{% block title %}Data Projects{% endblock %} -{% block subtitle %}Click on a project below to learn more{% endblock %} +{% block title %}Data Sets{% endblock %} +{% block subtitle %}Click on a data set below to learn more{% endblock %} {% block content %}
    From 77d980484833cd1815b66ba410ea19eaa7c3c737 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Thu, 26 Jul 2018 11:07:52 -0400 Subject: [PATCH 260/613] HYP-72: Add search to django admin --- app/projects/admin.py | 10 ++++++++++ app/projects/views.py | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/projects/admin.py b/app/projects/admin.py index 192b3ba9..beb475df 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import DataProject from .models import AgreementForm from .models import SignedAgreementForm @@ -14,6 +15,7 @@ class DataprojectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'is_contest') + list_filter = ('is_contest', ) class AgreementformAdmin(admin.ModelAdmin): list_display = ('name', 'short_name', 'type', 'form_file_path') @@ -23,9 +25,13 @@ class SignedagreementformAdmin(admin.ModelAdmin): class TeamAdmin(admin.ModelAdmin): list_display = ('team_leader', 'data_project') + list_filter = ('data_project', ) + search_fields = ('data_project__project_key', 'team_leader__email') class ParticipantAdmin(admin.ModelAdmin): list_display = ('user', 'data_challenge', 'team') + list_filter = ('data_challenge', ) + search_fields = ('data_challenge__project_key', 'team__team_leader__email', 'user__email') class InstitutionAdmin(admin.ModelAdmin): list_display = ('name', 'logo_path') @@ -35,12 +41,16 @@ class DataGateAdmin(admin.ModelAdmin): class HostedFileAdmin(admin.ModelAdmin): list_display = ('long_name', 'project', 'file_name', 'file_location_type', 'file_location') + list_filter = ('project', ) + search_fields = ('project__project_key', 'file_name', ) class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') class ParticipantSubmissionAdmin(admin.ModelAdmin): list_display = ('participant', 'upload_date', 'uuid', 'location', 'deleted') + list_filter = ('participant__data_challenge', ) + search_fields = ('participant__data_challenge__project_key', 'participant__user__email', ) class TeamSubmissionsDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'team', 'download_date') diff --git a/app/projects/views.py b/app/projects/views.py index 012832e2..3c504b8b 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -309,8 +309,7 @@ def list_data_challenges(request, template_name='datacontests/list.html'): # Build the dictionary with all project and permission information needed for data_contest in all_data_contests: - logger.debug(data_contest.name) - + # Package all the necessary information into one dictionary contest = {"name": data_contest.name, "short_description": data_contest.short_description, From 238512413eed42a7add3866a49bee73035d2f778 Mon Sep 17 00:00:00 2001 From: Nate Bessa Date: Fri, 17 Aug 2018 10:22:14 -0400 Subject: [PATCH 261/613] HYP-71: Refactoring submissions into ChallengeTasks N2C2 Track 2 required more than one type of submission: "tasks". This commit renames the ParticipantSubmission model to ChallengeTaskSubmission and adds a ChallengeTask model, of which a DataProject can have multiple. This then required changes to viewing, uploading, deleting, and downloading submissions, as well as a custom migration to preserve N2C2 Track 1 submissions that did not have a ChallengeTask concept at the time. Also includes a number of UI changes to the challenge participate screen. --- app/projects/admin.py | 17 +- .../migrations/0051_auto_20180813_1923.py | 33 +++ .../migrations/0052_auto_20180813_1928.py | 67 ++++++ .../migrations/0053_auto_20180813_2004.py | 26 +++ .../migrations/0054_auto_20180814_1515.py | 24 ++ .../migrations/0055_auto_20180814_1538.py | 19 ++ ..._dataproject_accepting_user_submissions.py | 19 ++ app/projects/models/__init__.py | 3 +- app/projects/models/models.py | 56 +++-- app/projects/urls.py | 8 +- app/projects/views.py | 78 +++---- app/projects/views_files.py | 194 +++++++++------ app/projects/views_manage.py | 6 +- app/projects/views_teams.py | 7 + app/templates/datacontests/manageteams.html | 4 +- .../email/email_submission_uploaded.html | 4 +- .../email/email_submission_uploaded.txt | 4 +- .../manage/manage_project_teams.html | 2 +- .../available_downloads.html | 4 + app/templates/project_participate/base.html | 4 +- .../signed_agreement_forms.html | 6 +- .../solutions_submitted.html | 113 --------- .../project_participate/submit_solution.html | 221 +++++++++++++----- .../project_participate/team_members.html | 30 +-- 24 files changed, 592 insertions(+), 357 deletions(-) create mode 100644 app/projects/migrations/0051_auto_20180813_1923.py create mode 100644 app/projects/migrations/0052_auto_20180813_1928.py create mode 100644 app/projects/migrations/0053_auto_20180813_2004.py create mode 100644 app/projects/migrations/0054_auto_20180814_1515.py create mode 100644 app/projects/migrations/0055_auto_20180814_1538.py create mode 100644 app/projects/migrations/0056_remove_dataproject_accepting_user_submissions.py delete mode 100644 app/templates/project_participate/solutions_submitted.html diff --git a/app/projects/admin.py b/app/projects/admin.py index beb475df..b5cd5b74 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -9,7 +9,8 @@ from .models import DataGate from .models import HostedFile from .models import HostedFileDownload -from .models import ParticipantSubmission +from .models import ChallengeTask +from .models import ChallengeTaskSubmission from .models import TeamSubmissionsDownload from .models import PayerDBForm @@ -47,10 +48,13 @@ class HostedFileAdmin(admin.ModelAdmin): class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') -class ParticipantSubmissionAdmin(admin.ModelAdmin): - list_display = ('participant', 'upload_date', 'uuid', 'location', 'deleted') - list_filter = ('participant__data_challenge', ) - search_fields = ('participant__data_challenge__project_key', 'participant__user__email', ) +class ChallengeTaskAdmin(admin.ModelAdmin): + list_display = ('data_project', 'title', 'enabled', 'opened_time', 'closed_time') + +class ChallengeTaskSubmissionAdmin(admin.ModelAdmin): + list_display = ('participant', 'challenge_task', 'upload_date', 'uuid') + list_filter = ('participant__data_challenge', 'challenge_task') + search_fields = ('participant__data_challenge__project_key', 'participant__user__email', 'challenge_task__title') class TeamSubmissionsDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'team', 'download_date') @@ -68,6 +72,7 @@ class PayerDBFormAdmin(admin.ModelAdmin): admin.site.register(DataGate, DataGateAdmin) admin.site.register(HostedFile, HostedFileAdmin) admin.site.register(HostedFileDownload, HostedFileDownloadAdmin) -admin.site.register(ParticipantSubmission, ParticipantSubmissionAdmin) +admin.site.register(ChallengeTask, ChallengeTaskAdmin) +admin.site.register(ChallengeTaskSubmission, ChallengeTaskSubmissionAdmin) admin.site.register(TeamSubmissionsDownload, TeamSubmissionsDownloadAdmin) admin.site.register(PayerDBForm, PayerDBFormAdmin) diff --git a/app/projects/migrations/0051_auto_20180813_1923.py b/app/projects/migrations/0051_auto_20180813_1923.py new file mode 100644 index 00000000..5415c2c2 --- /dev/null +++ b/app/projects/migrations/0051_auto_20180813_1923.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-13 19:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0050_auto_20180713_1346'), + ] + + operations = [ + migrations.CreateModel( + name='DataProjectSubmissionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(default=None, max_length=200)), + ('description', models.CharField(blank=True, max_length=2000, null=True)), + ('max_submissions', models.IntegerField(default=1)), + ('opened_time', models.DateTimeField(blank=True, null=True)), + ('closed_time', models.DateTimeField(blank=True, null=True)), + ('enabled', models.BooleanField(default=False)), + ('data_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.DataProject')), + ], + ), + migrations.RenameModel( + old_name='ParticipantSubmission', + new_name='DataProjectSubmission', + ), + ] diff --git a/app/projects/migrations/0052_auto_20180813_1928.py b/app/projects/migrations/0052_auto_20180813_1928.py new file mode 100644 index 00000000..065dd81a --- /dev/null +++ b/app/projects/migrations/0052_auto_20180813_1928.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-13 19:28 +from __future__ import unicode_literals + +from django.db import migrations, models + +def create_n2c2t1_submission_type(apps, schema_editor): + + DataProject = apps.get_model("projects", "dataproject") + DataProjectSubmissionType = apps.get_model("projects", "dataprojectsubmissiontype") + + n2c2t1_project = DataProject.objects.get(project_key="n2c2-t1") + + DataProjectSubmissionType.objects.create( + data_project=n2c2t1_project, + title='Task', + description='', + submission_form_file_path='n2c2-t1.html', + max_submissions=3, + opened_time=None, + closed_time=None, + enabled=True, + ) + + +def convert_dataprojectsubmissions(apps, schema_editor): + """ + At this time, all DataProjectSubmission records belong to the n2c2-t1 data project. + """ + + DataProject = apps.get_model("projects", "dataproject") + DataProjectSubmissionType = apps.get_model("projects", "dataprojectsubmissiontype") + DataProjectSubmission = apps.get_model("projects", "dataprojectsubmission") + + n2c2t1_project = DataProject.objects.get(project_key="n2c2-t1") + n2c2t1_submissiontype = DataProjectSubmissionType.objects.get(data_project=n2c2t1_project) + + # Set the submission type for every submission + for submission in DataProjectSubmission.objects.all(): + submission.dataprojectsubmissiontype = n2c2t1_submissiontype + submission.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0051_auto_20180813_1923'), + ] + + operations = [ + migrations.AddField( + model_name='dataprojectsubmissiontype', + name='submission_form_file_path', + field=models.CharField(max_length=300, blank=True, null=True), + ), + migrations.RemoveField( + model_name='dataproject', + name='submission_form_file_path', + ), + migrations.RunPython(create_n2c2t1_submission_type), + migrations.AddField( + model_name='dataprojectsubmission', + name='submission_form_file_path', + field=models.CharField(max_length=300, blank=True, null=True), + ), + migrations.RunPython(convert_dataprojectsubmissions), + ] diff --git a/app/projects/migrations/0053_auto_20180813_2004.py b/app/projects/migrations/0053_auto_20180813_2004.py new file mode 100644 index 00000000..d70ed3b5 --- /dev/null +++ b/app/projects/migrations/0053_auto_20180813_2004.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-13 20:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0052_auto_20180813_1928'), + ] + + operations = [ + migrations.RemoveField( + model_name='dataprojectsubmission', + name='submission_form_file_path', + ), + migrations.AddField( + model_name='dataprojectsubmission', + name='dataprojectsubmissiontype', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='projects.DataProjectSubmissionType'), + preserve_default=False, + ), + ] diff --git a/app/projects/migrations/0054_auto_20180814_1515.py b/app/projects/migrations/0054_auto_20180814_1515.py new file mode 100644 index 00000000..8c787663 --- /dev/null +++ b/app/projects/migrations/0054_auto_20180814_1515.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-14 15:15 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0053_auto_20180813_2004'), + ] + + operations = [ + migrations.RenameModel( + old_name='DataProjectSubmissionType', + new_name='ChallengeTask', + ), + migrations.RenameField( + model_name='dataprojectsubmission', + old_name='dataprojectsubmissiontype', + new_name='challenge_task', + ), + ] diff --git a/app/projects/migrations/0055_auto_20180814_1538.py b/app/projects/migrations/0055_auto_20180814_1538.py new file mode 100644 index 00000000..dd343624 --- /dev/null +++ b/app/projects/migrations/0055_auto_20180814_1538.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-14 15:38 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0054_auto_20180814_1515'), + ] + + operations = [ + migrations.RenameModel( + old_name='DataProjectSubmission', + new_name='ChallengeTaskSubmission', + ), + ] diff --git a/app/projects/migrations/0056_remove_dataproject_accepting_user_submissions.py b/app/projects/migrations/0056_remove_dataproject_accepting_user_submissions.py new file mode 100644 index 00000000..96133c9f --- /dev/null +++ b/app/projects/migrations/0056_remove_dataproject_accepting_user_submissions.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-17 13:57 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0055_auto_20180814_1538'), + ] + + operations = [ + migrations.RemoveField( + model_name='dataproject', + name='accepting_user_submissions', + ), + ] diff --git a/app/projects/models/__init__.py b/app/projects/models/__init__.py index 077efe24..5fb7fdf1 100644 --- a/app/projects/models/__init__.py +++ b/app/projects/models/__init__.py @@ -8,7 +8,8 @@ from .models import HostedFile from .models import HostedFileDownload from .models import TeamComment -from .models import ParticipantSubmission +from .models import ChallengeTask +from .models import ChallengeTaskSubmission from .models import ParticipantProject from .models import TeamSubmissionsDownload from .models import AGREEMENT_FORM_TYPE_STATIC diff --git a/app/projects/models/models.py b/app/projects/models/models.py index 2e90a162..a4326057 100644 --- a/app/projects/models/models.py +++ b/app/projects/models/models.py @@ -147,9 +147,6 @@ class DataProject(models.Model): is_contest = models.BooleanField(default=False, blank=False, null=False) has_teams = models.BooleanField(default=False, blank=False, null=False) - accepting_user_submissions = models.BooleanField(default=False, blank=False, null=False) - submission_form_file_path = models.CharField(max_length=300, blank=True, null=True) - show_jwt = models.BooleanField(default=False, blank=False, null=False) def __str__(self): @@ -187,30 +184,14 @@ class Team(models.Model): class Meta: unique_together = ('team_leader', 'data_project',) - def get_count_of_submissions_made(self): - """ - Returns the total number of submissions that a team's participants have made for its challenge. - """ - - submissions = self.get_submissions().count() - return submissions - - def get_number_of_submissions_left(self): - """ - Returns the number of submissions left that a team may make. - """ - - # TODO: abstract this number to the DataProjects class? - return 3 - self.get_count_of_submissions_made() - def get_submissions(self): """ - Returns a queryset of the non-deleted ParticipantSubmission records for this team. + Returns a queryset of the non-deleted ChallengeTaskSubmission records for this team. """ participants = self.participant_set.all() - return ParticipantSubmission.objects.filter( + return ChallengeTaskSubmission.objects.filter( participant__in=participants, deleted=False ) @@ -254,10 +235,10 @@ def set_approved(self): def get_submissions(self): """ - Returns a queryset of the non-deleted ParticipantSubmission records for this participant. + Returns a queryset of the non-deleted ChallengeTaskSubmission records for this participant. """ - return ParticipantSubmission.objects.filter( + return ChallengeTaskSubmission.objects.filter( participant=self, deleted=False ) @@ -265,6 +246,7 @@ def get_submissions(self): def __str__(self): return '%s - %s' % (self.user, self.data_challenge) + class HostedFile(models.Model): """ Tracks the files belonging to projects that users will be able to download. @@ -281,6 +263,7 @@ class HostedFile(models.Model): def __str__(self): return '%s - %s' % (self.project, self.long_name) + class HostedFileDownload(models.Model): """ Tracks who is attempting to download a hosted file. @@ -290,6 +273,7 @@ class HostedFileDownload(models.Model): hosted_file = models.ForeignKey(HostedFile) download_date = models.DateTimeField(auto_now_add=True) + class TeamComment(models.Model): user = models.ForeignKey(User) team = models.ForeignKey(Team) @@ -299,13 +283,35 @@ class TeamComment(models.Model): def __str__(self): return '%s %s %s' % (self.user, self.team, self.date) -class ParticipantSubmission(models.Model): + +class ChallengeTask(models.Model): + """ + Describes a task that a data challenge might require. User's submissions for tasks are captured + in the ChallengeTaskSubmission model. + """ + + data_project = models.ForeignKey(DataProject) + title = models.CharField(max_length=200, default=None, blank=False, null=False) + description = models.CharField(max_length=2000, blank=True, null=True) + submission_form_file_path = models.CharField(max_length=300, blank=True, null=True) + max_submissions = models.IntegerField(default=1, blank=False, null=False) + opened_time = models.DateTimeField(blank=True, null=True) + closed_time = models.DateTimeField(blank=True, null=True) + enabled = models.BooleanField(default=False, blank=False, null=False) + + def __str__(self): + return '%s: %s' % (self.data_project.project_key, self.title) + + +class ChallengeTaskSubmission(models.Model): """ Captures the files that participants are submitting for their challenges. Through the Participant model you can get to what team and project this submission pertains to. The location field is for fileservice integration. The submission_form_answers field stores any answers a participant might provide when submitting their work. """ + + challenge_task = models.ForeignKey(ChallengeTask) participant = models.ForeignKey(Participant) upload_date = models.DateTimeField(auto_now_add=True) uuid = models.UUIDField(null=False, unique=True, primary_key=True, default=None) @@ -331,5 +337,5 @@ class TeamSubmissionsDownload(models.Model): user = models.ForeignKey(User) team = models.ForeignKey(Team) - participant_submissions = models.ManyToManyField(ParticipantSubmission) + participant_submissions = models.ManyToManyField(ChallengeTaskSubmission) download_date = models.DateTimeField(auto_now_add=True) diff --git a/app/projects/urls.py b/app/projects/urls.py index 1b37b3bb..dc0fafbd 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -29,9 +29,9 @@ from .views_teams import download_signed_form from .views_files import download_dataset -from .views_files import upload_participantsubmission_file +from .views_files import upload_challengetasksubmission_file from .views_files import download_team_submissions -from .views_files import delete_participantsubmission +from .views_files import delete_challengetasksubmission urlpatterns = ( url(r'^$', list_data_projects), @@ -58,9 +58,9 @@ url(r'^download_signed_form/$', download_signed_form), url(r'^signed_agreement_form/$', signed_agreement_form), url(r'^download_dataset/$', download_dataset), - url(r'^upload_participantsubmission_file/$', upload_participantsubmission_file), + url(r'^upload_challengetasksubmission_file/$', upload_challengetasksubmission_file), url(r'^download_team_submissions/$', download_team_submissions), - url(r'^delete_participantsubmission/$', delete_participantsubmission), + url(r'^delete_challengetasksubmission/$', delete_challengetasksubmission), url(r'^download_email_list/$', download_email_list), url(r'^(?P[^/]+)/$', DataProjectView.as_view()), ) diff --git a/app/projects/views.py b/app/projects/views.py index 3c504b8b..c942b48e 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -29,6 +29,7 @@ from .models import Participant from .models import SignedAgreementForm +from .models import ChallengeTaskSubmission from .models import AGREEMENT_FORM_TYPE_STATIC from .models import AGREEMENT_FORM_TYPE_DJANGO @@ -469,21 +470,18 @@ def get_participate_context(self, context): context['left_panels'] = [] context['right_panels'] = [] - # Add a panel for displaying submitted solutions (if needed). - self.panel_submissions(context) - # Add a panel for displaying team members (if needed). self.panel_team_members(context) # Add a panel for displaying your signed agreement forms (if needed). self.panel_signed_agreement_forms(context) - # Add a panel for a solution submission form (if needed). - self.panel_submit_solution(context) - # Add a panel for available downloads. self.panel_available_downloads(context) + # Add a panel for a solution submission form (if needed). + self.panel_submit_task_solutions(context) + # Set the template that should be rendered. self.template_name = 'project_participate/base.html' @@ -701,34 +699,6 @@ def step_setup_team(self, context): context['steps'].append(step) - def panel_submissions(self, context): - """ - Builds the context needed for a user to view submitted solutions associated - with them. If this Participant has no team, then they will only see their - submissions. If this Participant does have a team, they will see the team's - submissions. This is an optional panel depending on the DataProject. - """ - - # Do not include this panel if this project does not accept submissions. - if not self.project.accepting_user_submissions: - return - - # Either get a team or individual's submissions - if self.participant.team is not None: - submissions = self.participant.team.get_submissions() - else: - submissions = self.participant.get_submissions() - - # Describe the panel. Include here any variables that the template will need. - panel = { - 'title': 'Solutions Submitted', - 'template': 'project_participate/solutions_submitted.html', - 'submissions': submissions - } - - # Add the panel to the left column. - context['left_panels'].append(panel) - def panel_team_members(self, context): """ Builds the context needed for a user to see who is on their team. This is @@ -776,22 +746,48 @@ def panel_signed_agreement_forms(self, context): # Add the panel to the left column. context['left_panels'].append(panel) - def panel_submit_solution(self, context): + def panel_submit_task_solutions(self, context): """ - Builds the context needed for a user to submit a solution for a data - challenge. This is an optional step depending on the DataProject. + Builds the context needed for a user to submit solutions for a data + challenge's task. A data challenge may require more than one solution. This + is an optional step depending on the DataProject. """ - # Do not include this panel if this project does not accept solutions. - if not self.project.accepting_user_submissions: + tasks = self.project.challengetask_set.all() + + # Do not include this panel if this project does not have any tasks + if tasks.count() == 0: return + task_details = [] + + for task in tasks: + + # Get the submissions for this task already submitted by the team. + submissions = ChallengeTaskSubmission.objects.filter( + challenge_task=task, + participant__in=self.participant.team.participant_set.all(), + deleted=False + ) + + total_submissions = submissions.count() + + task_context = { + 'task': task, + 'submissions': submissions, + 'total_submissions': total_submissions, + 'submissions_left': task.max_submissions - total_submissions + } + + task_details.append(task_context) + # Describe the panel. Include here any variables that the template will need. panel = { - 'title': 'Submit Your Solution', + 'title': 'Tasks to Complete', 'template': 'project_participate/submit_solution.html', 'team': self.participant.team, - 'project': self.project + 'project': self.project, + 'task_details': task_details } # Add the panel to the right column. diff --git a/app/projects/views_files.py b/app/projects/views_files.py index e0e82de3..044b1467 100644 --- a/app/projects/views_files.py +++ b/app/projects/views_files.py @@ -27,7 +27,8 @@ from projects.models import DataProject from projects.models import HostedFile from projects.models import HostedFileDownload -from projects.models import ParticipantSubmission +from projects.models import ChallengeTask +from projects.models import ChallengeTaskSubmission from projects.models import TeamSubmissionsDownload from projects.models import Participant from projects.models import Team @@ -105,14 +106,13 @@ def download_team_submissions(request): team = get_object_or_404(Team, data_project=project, team_leader__email=team_leader_email) team_participants = team.participant_set.all() - # Get all of the participant submission records belonging to this team, ordered by upload date - team_submissions = team.get_submissions().order_by('upload_date') - - logger.debug('Building a zip file containing the {submission_count} submissions from team {team}.'.format( - submission_count=team_submissions.count(), + logger.debug('Building a zip file containing the submissions from team {team}.'.format( team=team_leader_email )) + # Get all of the participant submission records belonging to this team, ordered by upload date + team_submissions = team.get_submissions().order_by('upload_date') + # Save a record of this person downloading this team's submissions file download_record = TeamSubmissionsDownload.objects.create( user=request.user, @@ -124,65 +124,95 @@ def download_team_submissions(request): download_record.save() # Create a dictory to hold the zipped submissions using a guid to keep it isolated from other requests - zipped_submissions_directory = "/tmp/" + str(uuid.uuid4()) + zipped_submissions_directory = "/tmp/zipped_submissions-" + str(uuid.uuid4()) if not os.path.exists(zipped_submissions_directory): os.makedirs(zipped_submissions_directory) - # A list of the file paths for each Hypatio-generated team submission zip file - zipped_submission_filepaths = [] - - for i, submission in enumerate(team_submissions): - submission_number = i + 1 - submission_date_string = datetime.strftime(submission.upload_date, "%Y%m%d_%H%M") - - # Create a working directory to hold the files specific to this submission that need to be zipped together - working_directory = "/tmp/" + str(uuid.uuid4()) - if not os.path.exists(working_directory): - os.makedirs(working_directory) + # A list of file paths to each task's zip file + zipped_tasks_filepath = [] - # Create a json file with the submission info string - info_file_name = "submission_info.json" - with open(working_directory + "/" + info_file_name, mode="w") as f: - f.write(submission.submission_info) + # Create subdirectories for each challenge task + for task in project.challengetask_set.all(): - # Get the submission file's byte contents from S3 - submission_file_download_url = fileservice.get_fileservice_download_url(request, submission.uuid) - submission_file_request = requests.get(submission_file_download_url) + # A list of file paths to each submission zip file for this task + zipped_task_submission_filepaths = [] - # Write the bytes to a zip file - submission_file_name = "submission_file.zip" - if submission_file_request.status_code == 200: - with open(working_directory + "/" + submission_file_name, mode="wb") as f: - f.write(submission_file_request.content) - else: - logger.error("[views_files][download_team_submissions] - Participant submission {uuid} file could not be pulled from S3.".format( - uuid=submission.uuid - )) - return HttpResponse("Error getting files", status=404) - - # Create the zip file and add the files to it - zip_file_path = zipped_submissions_directory + "/submission_" + str(submission_number) + "_" + submission_date_string + ".zip" - with zipfile.ZipFile(zip_file_path, mode="w") as zf: - zf.write(working_directory + "/" + info_file_name, arcname=info_file_name) - zf.write(working_directory + "/" + submission_file_name, arcname=submission_file_name) - - # Add the zipfile to the list of zip files - zipped_submission_filepaths.append(zip_file_path) + # Get the submissions for this task submitted by the team. + submissions = ChallengeTaskSubmission.objects.filter( + challenge_task=task, + participant__in=team.participant_set.all(), + deleted=False + ) - # Delete the working directory - shutil.rmtree(working_directory) + if submissions.count() > 0: + + for i, submission in enumerate(submissions): + submission_number = i + 1 + submission_date_string = datetime.strftime(submission.upload_date, "%Y%m%d_%H%M") + + # Create a working directory to hold the files specific to this submission that need to be zipped together + working_directory = "/tmp/working_dir-" + str(uuid.uuid4()) + if not os.path.exists(working_directory): + os.makedirs(working_directory) + + # Create a json file with the submission info string + info_file_name = "submission_info.json" + with open(working_directory + "/" + info_file_name, mode="w") as f: + f.write(submission.submission_info) + + # Get the submission file's byte contents from S3 + submission_file_download_url = fileservice.get_fileservice_download_url(request, submission.uuid) + submission_file_request = requests.get(submission_file_download_url) + + # Write the bytes to a zip file + submission_file_name = "submission_file.zip" + if submission_file_request.status_code == 200: + with open(working_directory + "/" + submission_file_name, mode="wb") as f: + f.write(submission_file_request.content) + else: + logger.error("[views_files][download_team_submissions] - Participant submission {uuid} file could not be pulled from S3.".format( + uuid=submission.uuid + )) + return HttpResponse("Error getting files", status=404) + + # Create the zip file and add the files to it + zip_file_path = zipped_submissions_directory + "/" + submission.challenge_task.title + "_" + str(submission_number) + "_" + submission_date_string + ".zip" + with zipfile.ZipFile(zip_file_path, mode="w") as zf: + zf.write(working_directory + "/" + info_file_name, arcname=info_file_name) + zf.write(working_directory + "/" + submission_file_name, arcname=submission_file_name) + + # Add the zipfile to the list of zip files + zipped_task_submission_filepaths.append(zip_file_path) + + # Delete the working directory + shutil.rmtree(working_directory) + + # Create a directory to store the encompassing task's zip file using a guid to keep it isolated from other requests + task_zip_file_directory = "/tmp/task_zip-" + str(uuid.uuid4()) + if not os.path.exists(task_zip_file_directory): + os.makedirs(task_zip_file_directory) + + # Create the encompassing task's zip file + task_zip_file_name = task.title + ".zip" + task_zip_file_path = os.path.join(task_zip_file_directory, task_zip_file_name) + with zipfile.ZipFile(task_zip_file_path, mode="w") as zf: + for submission_zip in zipped_task_submission_filepaths: + zf.write(submission_zip, arcname=os.path.basename(submission_zip)) + + # Add the zipfile to the list of zip files + zipped_tasks_filepath.append(task_zip_file_path) # Create a directory to store the encompassing zip file using a guid to keep it isolated from other requests - final_zip_file_directory = "/tmp/" + str(uuid.uuid4()) + final_zip_file_directory = "/tmp/final_zip-" + str(uuid.uuid4()) if not os.path.exists(final_zip_file_directory): os.makedirs(final_zip_file_directory) - # Create the encompassing zip file + # Combine all the zipped tasks into one file zip file final_zip_file_name = project_key + "_submissions_" + team_leader_email.replace('@', '-at-') + ".zip" final_zip_file_path = os.path.join(final_zip_file_directory, final_zip_file_name) with zipfile.ZipFile(final_zip_file_path, mode="w") as zf: - for submission_zip in zipped_submission_filepaths: - zf.write(submission_zip, arcname=os.path.basename(submission_zip)) + for task_zip in zipped_tasks_filepath: + zf.write(task_zip, arcname=os.path.basename(task_zip)) # Prepare the zip file to be served final_zip_file = open(final_zip_file_path, 'rb') @@ -192,44 +222,54 @@ def download_team_submissions(request): # Delete all the zip files from disk storage shutil.rmtree(final_zip_file_directory) shutil.rmtree(zipped_submissions_directory) + for path in zipped_tasks_filepath: + shutil.rmtree(os.path.dirname(path)) return response @user_auth_and_jwt -def upload_participantsubmission_file(request): +def upload_challengetasksubmission_file(request): """ On a POST, send metadata about the user's file to fileservice to get back an S3 upload link. On a PATCH, check to see that the file successfully was uploaded to S3 and then create a new - ParticipantSubmission record. + ChallengeTaskSubmission record. """ - logger.debug('upload_participantsubmission_file: {}'.format(request.method)) + logger.debug('upload_challengetasksubmission_file: {}'.format(request.method)) if request.method == 'POST': logger.debug('post') # Assembles the form and runs validation. filename = request.POST.get('filename') - project = request.POST.get('project') + project_key = request.POST.get('project_key') + task_id = request.POST.get('task_id') - if not filename or not project: - logger.error('No filename or no project!') - return HttpResponse('Filename and project are required', status=400) + if not filename or not project_key or not task_id: + logger.error('No filename, project, or task!') + return HttpResponse('Filename, project, task are required', status=400) # Check that user has permissions to be submitting files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) - if not sciauthz.user_has_single_permission(project, "VIEW"): - logger.debug("[views_files][upload_participantsubmission_file] - No Access for user " + request.user.email) + if not sciauthz.user_has_single_permission(project_key, "VIEW"): + logger.debug("[views_files][upload_challengetasksubmission_file] - No Access for user " + request.user.email) return HttpResponse("You do not have access to upload this file.", status=403) if filename.split(".")[-1] != "zip": logger.error('Not a zip file.') return HttpResponse("Only .zip files are accepted", status=400) + try: + task = ChallengeTask.objects.get(id=task_id) + except exceptions.ObjectDoesNotExist: + logger.error('Task not found with id {id}'.format(id=task_id)) + return HttpResponse('Task not found', status=400) + # Prepare the metadata. metadata = { - 'project': project, + 'project': project_key, + 'task': task.title, 'uploader': request.user.email, 'type': 'project_submission', 'app': 'hypatio', @@ -265,9 +305,10 @@ def upload_participantsubmission_file(request): submission_info = copy(data) # Get the participant. - project = get_object_or_404(DataProject, project_key=submission_info['project']) + project = get_object_or_404(DataProject, project_key=submission_info['project_key']) participant = get_object_or_404(Participant, user=request.user, data_challenge=project) team = participant.team + task = get_object_or_404(ChallengeTask, id=submission_info['task_id']) # Remove a few unnecessary fields. del submission_info['csrfmiddlewaretoken'] @@ -276,26 +317,37 @@ def upload_participantsubmission_file(request): # Add some more fields submission_info['submitted_by'] = request.user.email submission_info['team_leader'] = participant.team.team_leader.email + submission_info['task'] = task.title + submission_info['submitted_on'] = datetime.strftime(datetime.now(), "%Y%m%d_%H%M") submission_info_json = json.dumps(submission_info, indent=4) # Create the object and save UUID and location for future downloads. - ParticipantSubmission.objects.create( + ChallengeTaskSubmission.objects.create( + challenge_task=task, participant=participant, uuid=data['uuid'], location=data['location'], submission_info=submission_info_json ) + # Get the submissions for this task already submitted by the team. + total_submissions = ChallengeTaskSubmission.objects.filter( + challenge_task=task, + participant__in=team.participant_set.all(), + deleted=False + ).count() + # Send an email notification to team members about the submission. emails = [member.user.email for member in team.participant_set.all()] context = { 'submission_info': submission_info_json, 'challenge': project, + 'task': task.title, 'submitter': request.user.email, - 'max_submissions': 3, - 'submission_count': team.get_count_of_submissions_made() + 'max_submissions': task.max_submissions, + 'submission_count': total_submissions } try: @@ -328,9 +380,9 @@ def upload_participantsubmission_file(request): return HttpResponse("Invalid method", status=403) @user_auth_and_jwt -def delete_participantsubmission(request): +def delete_challengetasksubmission(request): """ - Marks a ParticipantSubmission as deleted so it will not be counted against their + Marks a ChallengeTaskSubmission as deleted so it will not be counted against their total submission count for a contest. """ @@ -341,20 +393,20 @@ def delete_participantsubmission(request): sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) submission_uuid = request.POST.get('submission_uuid') - submission = ParticipantSubmission.objects.get(uuid=submission_uuid) + submission = ChallengeTaskSubmission.objects.get(uuid=submission_uuid) project = submission.participant.data_challenge team = submission.participant.team if not sciauthz.user_has_single_permission(project.project_key, "VIEW"): logger.debug( - "[views_files][delete_participantsubmission] - No Access for user %s", + "[views_files][delete_challengetasksubmission] - No Access for user %s", request.user.email ) return HttpResponse("You do not have access to delete this file.", status=403) logger.debug( - '[views_files][delete_participantsubmission] - %s is trying to delete submission %s', + '[views_files][delete_challengetasksubmission] - %s is trying to delete submission %s', request.user.email, submission_uuid ) @@ -366,7 +418,7 @@ def delete_participantsubmission(request): # Check that the user is either the team leader, the original submitter, or a manager if not user_is_submitter and not user_is_team_leader and not user_is_manager: logger.debug( - "[views_files][delete_participantsubmission] - No Access for user %s", + "[views_files][delete_challengetasksubmission] - No Access for user %s", request.user.email ) return HttpResponse("Only the original submitter, team leader, or challenge manager may delete this.", status=403) @@ -385,7 +437,7 @@ def delete_participantsubmission(request): 'deleted_by': deleted_by, 'project': project.project_key, 'submission_uuid': submission_uuid, - 'submissions_left': team.get_number_of_submissions_left() + # 'submissions_left': team.get_number_of_submissions_left() } emails = [member.user.email for member in team.participant_set.all()] diff --git a/app/projects/views_manage.py b/app/projects/views_manage.py index 0b904ecc..2cccf32a 100644 --- a/app/projects/views_manage.py +++ b/app/projects/views_manage.py @@ -19,7 +19,7 @@ from .models import HostedFile from .models import HostedFileDownload from .models import Participant -from .models import ParticipantSubmission +from .models import ChallengeTaskSubmission from hypatio.scireg_services import get_distinct_countries_participating from hypatio.scireg_services import get_user_profile @@ -145,7 +145,7 @@ def manage_project_team(request, project_key, team_leader, template_name='manage @user_auth_and_jwt def manage_team(request, project_key, team_leader, template_name='datacontests/manageteams.html'): """ - Populates the team management modal popup on the contest management screen. + Populates the team management screen. """ user = request.user @@ -369,7 +369,7 @@ def manage_contest(request, project_key, template_name='datacontests/manageconte approved_participants = Participant.objects.filter(team__in=approved_teams) - all_submissions = ParticipantSubmission.objects.filter( + all_submissions = ChallengeTaskSubmission.objects.filter( participant__in=approved_participants, deleted=False ) diff --git a/app/projects/views_teams.py b/app/projects/views_teams.py index 9acd6c1c..18994d66 100644 --- a/app/projects/views_teams.py +++ b/app/projects/views_teams.py @@ -332,6 +332,13 @@ def leave_team(request): logger.error("[HYPATIO][leave_team] DataProject not found for given project_key: " + project_key) return HttpResponse(500) + # Remove VIEW permissions on the DataProject + # sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + # sciauthz.remove_view_permission(project_key, request.user.email) + + # TODO remove team leader's scireg permissions + # ... + participant = Participant.objects.get(user=request.user, data_challenge=project) participant.team = None participant.pending = False diff --git a/app/templates/datacontests/manageteams.html b/app/templates/datacontests/manageteams.html index a15b6052..1cc8b4b0 100644 --- a/app/templates/datacontests/manageteams.html +++ b/app/templates/datacontests/manageteams.html @@ -146,6 +146,7 @@

    Challenge Submissions

    PersonTask Date
    {{ upload.participant.user.email }}{{ upload.challenge_task.title }} {{ upload.upload_date|timezone:"America/New_York" }} (EST) @@ -268,7 +270,7 @@

    Previous comments:

    $.ajax({ method: "POST", data: {"submission_uuid": submission_uuid}, - url: "/delete_participantsubmission/", + url: "/delete_challengetasksubmission/", success: function (data, textStatus, jqXHR) { button.removeClass('btn-danger'); button.addClass('btn-success'); diff --git a/app/templates/email/email_submission_uploaded.html b/app/templates/email/email_submission_uploaded.html index 75eb2967..37c8e806 100644 --- a/app/templates/email/email_submission_uploaded.html +++ b/app/templates/email/email_submission_uploaded.html @@ -9,11 +9,11 @@

    - Team member {{ submitter }} has submitted a solution on behalf of your team for challenge {{ challenge.name }}. + Team member {{ submitter }} has submitted a solution to task {{ task }} for project {{ challenge.name }} on behalf of your team.

    - Out of {{ max_submissions }} max submissions, your team has now made {{ submission_count }} submissions. + Out of {{ max_submissions }} max submissions for this particular task, your team has now made {{ submission_count }} submissions. Your team may delete this or a previous submission and resubmit if you wish to.

    diff --git a/app/templates/email/email_submission_uploaded.txt b/app/templates/email/email_submission_uploaded.txt index 8e821409..854cbe32 100644 --- a/app/templates/email/email_submission_uploaded.txt +++ b/app/templates/email/email_submission_uploaded.txt @@ -1,4 +1,6 @@ -Team member {{ submitter }} has submitted a solution on behalf of your team. Out of {{ max_submissions }} max submissions, your team has now made {{ submission_count }} submissions. +Team member {{ submitter }} has submitted a solution to task {{ task }} for project {{ challenge.name }} on behalf of your team. + +Out of {{ max_submissions }} max submissions for this particular task, your team has now made {{ submission_count }} submissions. Your team may delete this or a previous submission and resubmit if you wish to. Submission metadata: {{ submission_info }} diff --git a/app/templates/manage/manage_project_teams.html b/app/templates/manage/manage_project_teams.html index f9491d3c..2b409ea8 100644 --- a/app/templates/manage/manage_project_teams.html +++ b/app/templates/manage/manage_project_teams.html @@ -177,7 +177,7 @@

    Previous comments:

    $.ajax({ method: "POST", data: {"submission_uuid": submission_uuid}, - url: "/delete_participantsubmission/", + url: "/delete_challengetasksubmission/", success: function (data, textStatus, jqXHR) { button.removeClass('btn-danger'); button.addClass('btn-success'); diff --git a/app/templates/project_participate/available_downloads.html b/app/templates/project_participate/available_downloads.html index acd9e3bc..e608bb79 100644 --- a/app/templates/project_participate/available_downloads.html +++ b/app/templates/project_participate/available_downloads.html @@ -1,3 +1,7 @@ +{% if panel.project.hostedfile_set.all|length == 0 %} + No files are available for download at this time. +{% endif %} + {% for file in panel.project.hostedfile_set.all %} {% if file.enabled or user.is_superuser %}

    {{ file.long_name }}

    diff --git a/app/templates/project_participate/base.html b/app/templates/project_participate/base.html index 896e7cd6..a438a065 100644 --- a/app/templates/project_participate/base.html +++ b/app/templates/project_participate/base.html @@ -17,7 +17,7 @@
    -
    +
    -
    +
    {% for panel in right_panels %}
    ",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ja(a),g,n,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ja(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function S(a,b){var c=a.oFeatures,d=c.bFilter; +c.bSort&&lb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;N(a);a._drawHold=!1}function mb(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k")[0]; +n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=nb(a);else if("f"==j&&d.bFilter)g=ob(a);else if("r"==j&&d.bProcessing)g=pb(a);else if("t"==j)g=qb(a);else if("i"==j&&d.bInfo)g=rb(a);else if("p"== +j&&d.bPaginate)g=sb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Qa(a,h(this).val());N(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function sb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){N(a)}, +b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display", +b?"block":"none");r(a,null,"processing",[a,b])}function qb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", +{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left", +0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:ka,sName:"scrolling"});return i[0]}function ka(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"), +j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),m=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,T=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),O,K,P,w,Ta=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};K=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==K&&a.scrollBarVis!==k)a.scrollBarVis=K,Y(a);else{a.scrollBarVis=K;p.children("thead, tfoot").remove(); +u&&(P=u.clone().prependTo(p),O=u.find("tr"),P=P.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");K=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=Z(a,b);c.style.width=a.aoColumns[B].sWidth});u&&H(function(a){a.style.width=""},P);f=p.outerWidth();if(""===c){r.width="100%";if(T&&(p.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width= +v(d),f=p.outerWidth());H(C,K);H(function(a){z.push(a.innerHTML);Ta.push(v(h(a).css("width")))},K);H(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ta[b]},o);h(K).height(0);u&&(H(C,P),H(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},P),H(function(a,b){a.style.width=y[b]},O),h(P).height(0));H(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.style.width=Ta[b]},K);u&&H(function(a,b){a.innerHTML='
    '+ +A[b]+"
    ";a.style.width=y[b]},P);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(T&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(O-b);(""===c||""!==d)&&J(a,1,"Possible column misalignment",6)}else O="100%";q.width=v(O);g.width=v(O);u&&(a.nScrollFoot.style.width=v(O));!e&&T&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();n[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+ +(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function H(a,b,c){for(var d=0,e=0,f=b.length,g,j;e").appendTo(j.find("tbody")); +j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ra(a,j.find("thead")[0]);for(m=0;m").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||G.body),d=c[0].offsetWidth;c.remove();return d}function Eb(a,b){var c=Fb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("
    ").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Fb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Hb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g, +"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Gb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!== +k&&h.extend(a.oPreviousSearch,Ab(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Ma(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ha(a,b){var c=[],c=Kb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0, +c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Wa)},"html-num":function(b){return za(b,a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Wa)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Lb(a){return function(){var b= +[ya(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing= +function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ka(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)}; +this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase(); +return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])}; +this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Lb(e));this.each(function(){var e={},g=1").appendTo(q));p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("
    {% for form in participant_info.signed_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} @@ -612,7 +612,7 @@ var form = data[index]; // Set the base URL for the forms - var formUrl = '{% url "projects:signed_agreement_form" %}?project_key={{ project_key }}&signed_form_id=' + form['id']; + var formUrl = '{% url "projects:signed_agreement_form" %}?project_key=' + form['project'] + '&signed_form_id=' + form['id']; // Set form type and icon classes var formClasses = (function(formStatus) { diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index 8adb3068..c513b383 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -177,7 +177,7 @@

    Team members

    {% for form in member.signed_agreement_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} From 92cfe54896267f68ebc0b1026955253af2e4259b Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 6 Jan 2022 10:18:50 -0500 Subject: [PATCH 462/613] HYP-260 - Fixed bug in URL resolvers --- app/templates/manage/project-base.html | 2 +- app/templates/manage/team.html | 2 +- .../projects/participate/signed-agreement-forms.html | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/templates/manage/project-base.html b/app/templates/manage/project-base.html index 86a56309..33763d69 100644 --- a/app/templates/manage/project-base.html +++ b/app/templates/manage/project-base.html @@ -364,7 +364,7 @@

    {% for form in participant_info.signed_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index c513b383..49588b4a 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -177,7 +177,7 @@

    Team members

    {% for form in member.signed_agreement_forms %} - + {{ form.agreement_form.short_name }} {% endfor %} diff --git a/app/templates/projects/participate/signed-agreement-forms.html b/app/templates/projects/participate/signed-agreement-forms.html index 10dfcdfd..3743ee47 100644 --- a/app/templates/projects/participate/signed-agreement-forms.html +++ b/app/templates/projects/participate/signed-agreement-forms.html @@ -7,7 +7,7 @@

    {{ signed_form.agreement_form.name }}

    Signed {{ signed_form.date_signed|timezone:"America/New_York" }} (EST)
    - View + View
    -{% endfor %} \ No newline at end of file +{% endfor %} From 28e8ff3b1c92e24eb388d78fb8163c69f449b600 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 6 Jan 2022 10:20:32 -0500 Subject: [PATCH 463/613] dev(gitignore): gitignore addition --- .gitignore | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 5d89396e..719aca1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -app/assets/* -*.pyc -*/.DS_Store -.DS_Store -*.log -app/db.sqlite3 -app/hypatio/local_settings.py +app/assets/* +*.pyc +*/.DS_Store +.DS_Store +*.log +app/db.sqlite3 +app/hypatio/local_settings.py .vscode/settings.json +backup From 29869806fdfd45b80f2ce53f9ef447714ec4d6b2 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 7 Jan 2022 12:12:41 -0500 Subject: [PATCH 464/613] HYP-257 - Added button to directly message team leaders --- app/manage/forms.py | 8 ++ app/manage/urls.py | 2 + app/manage/views.py | 129 +++++++++++++++++- .../email/email_team_notification.html | 17 +++ .../email/email_team_notification.txt | 8 ++ app/templates/manage/notification.html | 22 +++ app/templates/manage/team.html | 56 +++++++- 7 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 app/templates/email/email_team_notification.html create mode 100644 app/templates/email/email_team_notification.txt create mode 100644 app/templates/manage/notification.html diff --git a/app/manage/forms.py b/app/manage/forms.py index c92240e2..f2f46ae4 100644 --- a/app/manage/forms.py +++ b/app/manage/forms.py @@ -5,6 +5,7 @@ from projects.models import DataProject from projects.models import HostedFile +from projects.models import Team # TODO Convert all other manual forms into Django forms # ... @@ -55,3 +56,10 @@ class Meta: 'hostedfileset': autocomplete.ModelSelect2(url='projects:hostedfileset-autocomplete', forward=['project'], attrs={'class': 'form-control form-control-select2'}) } +class NotificationForm(forms.Form): + """ + Determines the fields that will appear. + """ + project = forms.ModelChoiceField(queryset=DataProject.objects.all(), widget=forms.HiddenInput) + message = forms.CharField(label='Message', required=True, widget=forms.Textarea) + team = forms.ModelChoiceField(queryset=Team.objects.all(), widget=forms.HiddenInput) diff --git a/app/manage/urls.py b/app/manage/urls.py index a0b5ef98..911bd1a9 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -5,6 +5,7 @@ from manage.views import DataProjectManageView from manage.views import manage_team from manage.views import ProjectParticipants +from manage.views import team_notification from manage.api import set_dataproject_details from manage.api import set_dataproject_registration_status @@ -45,6 +46,7 @@ url(r'^save-team-comment/$', save_team_comment, name='save-team-comment'), url(r'^set-team-status/$', set_team_status, name='set-team-status'), url(r'^delete-team/$', delete_team, name='delete-team'), + url(r'^team-notification/$', team_notification, name='team-notification'), url(r'^download-team-submissions/(?P[^/]+)/(?P[^/]+)/$', download_team_submissions, name='download-team-submissions'), url(r'^download-submission/(?P[^/]+)/$', download_submission, name='download-submission'), url(r'^host-submission/(?P[^/]+)/$', host_submission, name='host-submission'), diff --git a/app/manage/views.py b/app/manage/views.py index 2826c4ca..4e72a65d 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -7,16 +7,21 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count from django.db.models import F -from django.http import HttpResponse, JsonResponse +from django.http import HttpResponse, JsonResponse, HttpResponseRedirect +from django.contrib import messages from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import TemplateView from django.views.generic.base import View from django.core.paginator import Paginator +from django.urls import reverse +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string from hypatio.sciauthz_services import SciAuthZ from hypatio.scireg_services import get_user_profile, get_distinct_countries_participating +from manage.forms import NotificationForm from projects.models import ChallengeTaskSubmission from projects.models import DataProject from projects.models import Participant @@ -353,6 +358,128 @@ def get(self, request, project_key, *args, **kwargs): return JsonResponse(data=data) +@user_auth_and_jwt +def team_notification(request, project_key=None): + """ + Manages sending notifications to team leaders + + :param request: The current HTTP request + :type request: HttpRequest + """ + # If this is a POST request we need to process the form data. + if request.method == 'POST': + logger.debug(f"Team notification: POST") + + # Process the form. + form = NotificationForm(request.POST) + if form.is_valid(): + + # Get the project + project = form.cleaned_data['project'] + team = form.cleaned_data['team'] + + # Form the context. + context = { + 'administrator_message': form.cleaned_data['message'], + 'project': project, + 'team': team, + 'site_url': settings.SITE_URL + } + + # Send it out. + email_template='team_notification' + subject='DBMI Portal - Team Notification' + + # Render templates + msg_html = render_to_string('email/email_team_notification.html', context) + msg_plain = render_to_string('email/email_team_notification.txt', context) + + try: + msg = EmailMultiAlternatives(subject=subject, + body=msg_plain, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[team.team_leader.email]) + msg.attach_alternative(msg_html, "text/html") + msg.send() + + # Handle outcome + if request.is_ajax(): + return HttpResponse('SUCCESS', status=200) + else: + # Set a message. + messages.success(request, 'Thanks, your notification has been sent!') + + except Exception as ex: + logger.exception(ex, exc_info=True, extra={ + 'email': email_template, 'extra': context + }) + + # Check how the request was made. + if request.is_ajax(): + return HttpResponse('ERROR', status=500) + else: + messages.error(request, 'An unexpected error occurred, please try again') + + # Send them back + return HttpResponseRedirect(reverse( + 'projects:view-project', + kwargs={'project_key': form.cleaned_data['project']} + )) + else: + logger.error(f"Invalid team notification form", extra={ + 'request': request, 'errors': form.errors.as_json(), + }) + + # Check how the request was made. + if request.is_ajax(): + return HttpResponse(form.errors.as_json(), status=500) + else: + messages.error(request, 'The form was invalid, please try again') + return HttpResponseRedirect(reverse( + 'projects:view-project', + kwargs={'project_key': form.cleaned_data['project']} + )) + + logger.debug(f"Team notification: GET") + + # If a GET (or any other method) we'll create a blank form. + initial = {} + + # If a project key was supplied and it matches a real project, pre-populate the form with it. + try: + if project_key: + data_project = DataProject.objects.get(project_key=project_key) + else: + data_project = DataProject.objects.get(id=request.GET["project"]) + + initial['project'] = data_project + except ObjectDoesNotExist: + logger.exception(f"Could not determine project", exc_info=True, extra={ + 'request': request, + }) + if request.is_ajax(): + return HttpResponse('The project could not be determined, cannot send message.', status=500) + else: + messages.error(request, 'The project could not be determined, cannot send message.') + + # Get the team + try: + team = Team.objects.get(id=request.GET["team"]) + initial['team'] = team + except ObjectDoesNotExist: + logger.exception(f"Could not determine team leader", exc_info=True, extra={ + 'request': request, + }) + if request.is_ajax(): + return HttpResponse('The team leader could not be determined, cannot send message.', status=500) + else: + messages.error(request, 'The team leader could not be determined, cannot send message.') + + # Generate and render the form. + form = NotificationForm(initial=initial) + return render(request, 'manage/notification.html', {'notification_form': form}) + + @user_auth_and_jwt def manage_team(request, project_key, team_leader, template_name='manage/team.html'): """ diff --git a/app/templates/email/email_team_notification.html b/app/templates/email/email_team_notification.html new file mode 100644 index 00000000..fce4d511 --- /dev/null +++ b/app/templates/email/email_team_notification.html @@ -0,0 +1,17 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Team Deleted{% endblock %} + +{% block content %} + +

    Hi,

    + +

    This is a notification that a {{ project.name }} challenge administrator has a message for your team.

    + +

    {{ administrator_message|linebreaks }}

    + +

    You can click here to return to the {{ project.name }} challenge page.

    + +

    Thank you.

    + +{% endblock %} diff --git a/app/templates/email/email_team_notification.txt b/app/templates/email/email_team_notification.txt new file mode 100644 index 00000000..bf795a20 --- /dev/null +++ b/app/templates/email/email_team_notification.txt @@ -0,0 +1,8 @@ +This is a notification that a {{ project.name }} challenge administrator has a message for your team. + +"{{ administrator_message }}" + +You can use the following link to return to the {{ project.name }} challenge page: +{{ site_url }}projects/{{ project.project_key }}/ + +Thank you. diff --git a/app/templates/manage/notification.html b/app/templates/manage/notification.html new file mode 100644 index 00000000..f7408fad --- /dev/null +++ b/app/templates/manage/notification.html @@ -0,0 +1,22 @@ +{% load bootstrap3 %} + + diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index 49588b4a..dce7c5ce 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -27,7 +27,7 @@
    + + +{# Add a placeholder for the modal contact form #} + {% endblock %} {% block footerscripts %} @@ -442,6 +457,45 @@

    Previous comments:

    alert('Failed to delete team.'); }); }); + + // AJAX for posting + $("#notification-form-button").on('click', function () { + + var url = "{% url 'manage:team-notification' %}?project={{ project.id }}&team={{ team.id }}"; + + $.ajax({ + type: 'GET', + url: url, + success: function (data, textStatus, jqXHR) { + $('#notification-form-modal').html(data); + $('#notification-form-modal').modal('show'); + }, + error : function(xhr,errmsg,err) { + console.log(xhr.status + ": " + xhr.responseText); + } + }); + return false; +}); + +$('#notification-form-modal').on('submit', '#notification-form', function() { + $.ajax({ + url : "{% url 'manage:team-notification' %}", + type : "POST", + data: $(this).serialize(), + context: this, + success : function(json) { + $('#notification-form-modal').modal('hide'); + notify('success', 'Thanks, your message has been submitted!', 'thumbs-up'); + }, + error : function(xhr,errmsg,err) { + $('#notification-form-modal').modal('hide'); + notify('danger', 'Something happened, please try again', 'exclamation-sign'); + console.log(xhr.status + ": " + xhr.responseText); + } + }); + return false; +}); + From 13296d0f4546536c343e69f5b387b265232b3845 Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 10 Jan 2022 12:06:03 +0000 Subject: [PATCH 465/613] fix(requirements): Updated Python requirements --- requirements.txt | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/requirements.txt b/requirements.txt index 07e09c7e..e7cd9205 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,17 +4,17 @@ # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in # -awscli==1.22.26 \ - --hash=sha256:6ad98ba18ee14cc51f7355ca368f9df01f6f983e5458ed62d0ea6436e8b440eb \ - --hash=sha256:cd4f526b492bb1491b01360e08d9e0abe309d783dbeebbbfc2b733a74c370542 +awscli==1.22.31 \ + --hash=sha256:0fb7beb560df747ba4005f9787002022e369ef3b6ed5335e0b5eb013252a750c \ + --hash=sha256:e826bf74c32ba09cf9fc45701038d0dc3b72489149ce72637231300c3cf10a48 # via -r requirements.in -boto3==1.20.26 \ - --hash=sha256:9c13f5c8fadf29088fac5feab849399169b6e8438c3b9a2310abdb7e5013ab65 \ - --hash=sha256:e8787a7f7c212d5b469dd8b998560c1b8e63badad5ceefb8331f4580386af044 +boto3==1.20.31 \ + --hash=sha256:3003d64ebef678b89a9909d2df3836160c7cbad5cbfe6c995a61de0875b36237 \ + --hash=sha256:948e81af347085e6bc5ff08368d7901afec9e7628adf180c9cc856c7b0ae3395 # via -r requirements.in -botocore==1.23.26 \ - --hash=sha256:0a933e3af6ecf79666beb2dfcb52a60f8ad1fee7df507f2a9202fe26fe569483 \ - --hash=sha256:298f4d4e29504f65f73e8f78084f830af45cec49087d7d8fcf09481e243b26ec +botocore==1.23.31 \ + --hash=sha256:187c736ce242bbea3d1440c580d270e0fd839276c5cc3938a85b8c59366c1803 \ + --hash=sha256:bb34fa60ab894f9a4a1f7de36e32a68ce17f302108f83732c3ca99c7da2bf68c # via # awscli # boto3 @@ -75,9 +75,9 @@ cffi==1.15.0 \ --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 # via cryptography -charset-normalizer==2.0.9 \ - --hash=sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721 \ - --hash=sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c +charset-normalizer==2.0.10 \ + --hash=sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd \ + --hash=sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455 # via requests colorama==0.4.3 \ --hash=sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff \ @@ -112,9 +112,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==2.2.25 \ - --hash=sha256:08bad7ef7e90286b438dbe1412c3e633fbc7b96db04735f0c7baadeed52f3fad \ - --hash=sha256:b1e65eaf371347d4b13eb7e061b09786c973061de95390c327c85c1e2aa2349c +django==2.2.26 \ + --hash=sha256:85e62019366692f1d5afed946ca32fef34c8693edf342ac9d067d75d64faf0ac \ + --hash=sha256:dfa537267d52c6243a62b32855a744ca83c37c70600aacffbfd98bc5d6d8518f # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -283,9 +283,9 @@ raven==6.10.0 \ # via # -r requirements.in # django-dbmi-client -requests==2.26.0 \ - --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ - --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d # via # -r requirements.in # django-dbmi-client @@ -312,9 +312,9 @@ sqlparse==0.4.2 \ --hash=sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae \ --hash=sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d # via django -urllib3==1.26.7 \ - --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece \ - --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 +urllib3==1.26.8 \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c # via # botocore # requests From 69048e5b1456e8c3098deb56f9b1f12a3fb095e3 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 10 Jan 2022 13:07:17 -0500 Subject: [PATCH 466/613] HYP-255/252: Added UW DUA, revused MIMIC3 DUA step --- app/projects/api.py | 8 ------ app/projects/models.py | 2 ++ app/projects/views.py | 9 ------- ...ernal DUA NLP Challenge - UW signed RE.pdf | Bin 0 -> 410906 bytes ...External DUA NLP Challenge - UW signed.pdf | Bin 0 -> 257445 bytes app/static/agreementforms/mimic3-dua.html | 13 +++++++++ app/static/agreementforms/uw-dua.html | 10 +++++++ .../signup/upload-agreement-form.html | 25 +++++++----------- 8 files changed, 34 insertions(+), 33 deletions(-) create mode 100644 app/static/agreementforms/UW External DUA NLP Challenge - UW signed RE.pdf create mode 100644 app/static/agreementforms/UW External DUA NLP Challenge - UW signed.pdf create mode 100644 app/static/agreementforms/mimic3-dua.html create mode 100644 app/static/agreementforms/uw-dua.html diff --git a/app/projects/api.py b/app/projects/api.py index 63d6cc56..a4e42796 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -775,7 +775,6 @@ def upload_signed_agreement_form(request): agreement_form_id = request.POST['agreement_form_id'] project_key = request.POST['project_key'] agreement_text = request.POST['agreement_text'] - email = request.POST['email'] agreement_form = AgreementForm.objects.get(id=agreement_form_id) project = DataProject.objects.get(project_key=project_key) @@ -802,11 +801,4 @@ def upload_signed_agreement_form(request): ) signed_agreement_form.save() - # Save fields - signed_agreement_form_fields = models.MIMIC3SignedAgreementFormFields( - signed_agreement_form=signed_agreement_form, - email=email, - ) - signed_agreement_form_fields.save() - return HttpResponse(status=200) diff --git a/app/projects/models.py b/app/projects/models.py index 867d07b6..288afb42 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -103,6 +103,8 @@ def clean(self): raise ValidationError("If the form type is external link, the external link field should be populated.") if self.type == AGREEMENT_FORM_TYPE_MODEL and not self.content: raise ValidationError("If the form type is model, the content field should be populated with the agreement form's HTML.") + if self.type == AGREEMENT_FORM_TYPE_FILE and not self.content and not self.form_file_path: + raise ValidationError("If the form type is file, the content field should be populated with the agreement form's HTML.") class DataProject(models.Model): diff --git a/app/projects/views.py b/app/projects/views.py index 32761fba..299d97f1 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -67,15 +67,6 @@ def signed_agreement_form(request): # find a better solution and have more time, this is it. signed_agreement_form_fields = {} - # Check MIMIC3 first. - fields = MIMIC3SignedAgreementFormFields.objects.filter(signed_agreement_form=signed_form).first() - if fields: - - # Create a dictionary of the values - signed_agreement_form_fields = { - "Email Address": fields.email, - } - if is_manager or signed_form.user == request.user: template_name = "projects/participate/view-signed-agreement-form.html" filled_out_signed_form = None diff --git a/app/static/agreementforms/UW External DUA NLP Challenge - UW signed RE.pdf b/app/static/agreementforms/UW External DUA NLP Challenge - UW signed RE.pdf new file mode 100644 index 0000000000000000000000000000000000000000..394bf6848fe591a7ebac275866fcf1fda2c70b98 GIT binary patch literal 410906 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

    ATa^Tma|^+BFt(RTy_164I*GDaF&U- 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≫v*~J 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%0Ae# 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

      k1aSpC-N0W$1t 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

      $IpWQpw|})S#m{JU?vfhK)f;sI$U~YdyeO!QT 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 pC*&!ouVtR|=Ulr^ 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%0D& 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

      -)dA>e0*mQ-Rcg49U%GnnU=q%VR? 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

      U+de+8D(&7XA6V9}x`SH?eOt5I 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%0AnJxWMPZ@#?T-W6ax#b2VCy!` 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 z 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 OB^ei&vaSjx+5Z8x=m&BD literal 0 HcmV?d00001 diff --git a/app/static/agreementforms/mimic3-dua.html b/app/static/agreementforms/mimic3-dua.html new file mode 100644 index 00000000..67b3b8f8 --- /dev/null +++ b/app/static/agreementforms/mimic3-dua.html @@ -0,0 +1,13 @@ +{% load static %} +

      Upload a 2-page PDF of the following:

      +
        +
      • Page 1 — PhysioNet "My Credentialing Applications"

      • +
      • Page 2 — Data Use Agreement for the MIMIC-III Clinical Database (v1.4)

      • +
      +

      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�+7y5Z07vPkonZObpAne$T1e82=kGm)iv& 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

    + diff --git a/app/static/agreementforms/uw-dua.html b/app/static/agreementforms/uw-dua.html new file mode 100644 index 00000000..a3c56706 --- /dev/null +++ b/app/static/agreementforms/uw-dua.html @@ -0,0 +1,10 @@ +{% load static %} +

    Download the following PDF, sign it, and upload a scan of the signed document.

    +
    + +
    diff --git a/app/templates/projects/signup/upload-agreement-form.html b/app/templates/projects/signup/upload-agreement-form.html index d7c0753f..7a784298 100644 --- a/app/templates/projects/signup/upload-agreement-form.html +++ b/app/templates/projects/signup/upload-agreement-form.html @@ -6,22 +6,15 @@
    -

    Upload a 2-page PDF of the following:

    -
      -
    • Page 1 — PhysioNet "My Credentialing Applications"

    • -
    • Page 2 — Data Use Agreement for the MIMIC-III Clinical Database (v1.4)

    • -
    -
    - -
    -
    - - + + {# Check source of agreement form content #} + {% if panel.additional_context.agreement_form.content %} + {{ panel.additional_context.agreement_form.content | safe }} + {% elif panel.additional_context.agreement_form.form_file_path %} + {{ panel.additional_context.agreement_form.form_file_path|get_html_form_file_contents | safe }} + {% else %} + This agreement form is missing content + {% endif %}
    From 424cf9975aa257d48aa45ff549faada5068c59fe Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 10 Jan 2022 13:13:39 -0500 Subject: [PATCH 467/613] HYP-255 - Fixed closing div tag on DUA upload step --- app/static/agreementforms/mimic3-dua.html | 18 +++++++++--------- app/static/agreementforms/uw-dua.html | 13 +++++++------ .../projects/signup/upload-agreement-form.html | 1 + 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/static/agreementforms/mimic3-dua.html b/app/static/agreementforms/mimic3-dua.html index 67b3b8f8..e61a55f5 100644 --- a/app/static/agreementforms/mimic3-dua.html +++ b/app/static/agreementforms/mimic3-dua.html @@ -1,13 +1,13 @@ {% load static %}

    Upload a 2-page PDF of the following:

    -
      -
    • Page 1 — PhysioNet "My Credentialing Applications"

    • -
    • Page 2 — Data Use Agreement for the MIMIC-III Clinical Database (v1.4)

    • -
    -
    +
      +
    • Page 1 — PhysioNet "My Credentialing Applications"

    • +
    • Page 2 — Data Use Agreement for the MIMIC-III Clinical Database (v1.4)

    • +
    diff --git a/app/static/agreementforms/uw-dua.html b/app/static/agreementforms/uw-dua.html index a3c56706..32d386e4 100644 --- a/app/static/agreementforms/uw-dua.html +++ b/app/static/agreementforms/uw-dua.html @@ -1,10 +1,11 @@ {% load static %}

    Download the following PDF, sign it, and upload a scan of the signed document.

    - +
    + View sample +   |   + Download +   |   + View complete instructions +
    diff --git a/app/templates/projects/signup/upload-agreement-form.html b/app/templates/projects/signup/upload-agreement-form.html index 7a784298..9c8757fe 100644 --- a/app/templates/projects/signup/upload-agreement-form.html +++ b/app/templates/projects/signup/upload-agreement-form.html @@ -15,6 +15,7 @@ {% else %} This agreement form is missing content {% endif %} +
    From caa7bdb984882224801f513e9682c441f3cd7e1c Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 10 Jan 2022 13:22:56 -0500 Subject: [PATCH 468/613] HYP-255 - UW DUA content update --- app/static/agreementforms/uw-dua.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/agreementforms/uw-dua.html b/app/static/agreementforms/uw-dua.html index 32d386e4..78a4e8f0 100644 --- a/app/static/agreementforms/uw-dua.html +++ b/app/static/agreementforms/uw-dua.html @@ -1,11 +1,11 @@ {% load static %} -

    Download the following PDF, sign it, and upload a scan of the signed document.

    +

    Please download the following PDF, complete and sign it, then upload the completed/signed copy. You may either a) use Adobe Acrobat to complete/sign the form electronically or b) print a hard-copy, complete/sign it by hand, and scan it.

    From 61fb4b2f10da278268ea83acefef16b83e670921 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 10 Jan 2022 17:10:37 -0500 Subject: [PATCH 469/613] HYP-259 - Fixed team syncing bug; ensures Participants are created for each project --- app/projects/signals.py | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/app/projects/signals.py b/app/projects/signals.py index e666d694..0e8f0972 100644 --- a/app/projects/signals.py +++ b/app/projects/signals.py @@ -62,33 +62,49 @@ def sync_teams(project): for sharing_project in DataProject.objects.filter(teams_source=project): # Check if already created - if Team.objects.filter(data_project=sharing_project, source=team).exists(): + shared_team = Team.objects.filter(data_project=sharing_project, source=team).first() + if not shared_team: + logger.debug(f"Team Sync: Team/{team} not shared with DataProject/{sharing_project}, creating") + + # Create the team + shared_team = Team( + source=team, + data_project=sharing_project, + team_leader=team.team_leader, + status=TEAM_READY, + ) + shared_team.save() + + else: logger.debug(f"Team Sync: Team/{team} already shared with DataProject/{sharing_project}") - continue - - # Create the team - shared_team = Team( - source=team, - data_project=sharing_project, - team_leader=team.team_leader, - status=TEAM_READY, - ) - shared_team.save() # Iterate participants in the source team for participant in team.participant_set.all(): - # Create a new one - shared_participant = Participant( + # See if they already exist + shared_participant = Participant.objects.filter( user=participant.user, project=sharing_project, team=shared_team, - team_wait_on_leader_email=participant.team_wait_on_leader_email, - team_wait_on_leader=participant.team_wait_on_leader, - team_pending=participant.team_pending, - team_approved=participant.team_approved, ) - shared_participant.save() + + if not shared_participant: + logger.debug(f"Team Sync: Participant/{participant} not shared with DataProject/{sharing_project}, creating") + + # Create a new one + shared_participant = Participant( + user=participant.user, + project=sharing_project, + team=shared_team, + team_wait_on_leader_email=participant.team_wait_on_leader_email, + team_wait_on_leader=participant.team_wait_on_leader, + team_pending=participant.team_pending, + team_approved=participant.team_approved, + ) + shared_participant.save() + + else: + logger.debug(f"Team Sync: Participant/{participant} already shared with DataProject/{sharing_project}") # Load deactivated teams for the source project that have been copied logger.debug("Team Sync: Processing deactivated teams") From bd17fb017b9247e5badf4417480acd564ea6d690 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 10 Jan 2022 19:27:42 -0500 Subject: [PATCH 470/613] HYP-255 - Removed sample link from UW DUA --- app/static/agreementforms/uw-dua.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/static/agreementforms/uw-dua.html b/app/static/agreementforms/uw-dua.html index 78a4e8f0..913120af 100644 --- a/app/static/agreementforms/uw-dua.html +++ b/app/static/agreementforms/uw-dua.html @@ -2,8 +2,6 @@

    Please download the following PDF, complete and sign it, then upload the completed/signed copy. You may either a) use Adobe Acrobat to complete/sign the form electronically or b) print a hard-copy, complete/sign it by hand, and scan it.

    - View sample -   |   Download   |   View complete instructions From 217644e171e5bbe6fda3fcdbba1433a298a7e2d6 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 11 Jan 2022 06:11:50 -0500 Subject: [PATCH 471/613] HYP-260 - Added team requirements notification --- app/templates/projects/signup/setup-team.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/templates/projects/signup/setup-team.html b/app/templates/projects/signup/setup-team.html index a199fc41..26e370e4 100644 --- a/app/templates/projects/signup/setup-team.html +++ b/app/templates/projects/signup/setup-team.html @@ -14,7 +14,7 @@ {% else %} {% endif %} @@ -101,7 +101,7 @@

    Joining a team?

    - +

    Leading a team?

    @@ -116,7 +116,7 @@

    Leading a team?

    {{ export.request_date|date:"c" }} {{ export.uuid }} - + Download {{ export.request_date|date:"c" }} {{ export.uuid }} - +
    {% if member.access_granted %} @@ -527,4 +531,12 @@

    Previous comments:

    }); + +{# Add a placeholder for any modal dialogs #} + {% endblock %} diff --git a/app/templates/manage/upload-signed-agreement-form.html b/app/templates/manage/upload-signed-agreement-form.html new file mode 100644 index 00000000..795486a1 --- /dev/null +++ b/app/templates/manage/upload-signed-agreement-form.html @@ -0,0 +1,20 @@ +{% load bootstrap3 %} + + + + + + From 44962f90646a5da6afc0d5aa92c95d56f88338df Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 11 Aug 2022 09:11:48 -0600 Subject: [PATCH 545/613] HYP-286 - Refactored submission system to allow configurable file types --- app/projects/api.py | 7 +- .../migrations/0093_auto_20220810_1356.py | 23 ++ app/projects/models.py | 20 ++ .../projects/participate/complete-tasks.html | 210 +++++++++--------- 4 files changed, 153 insertions(+), 107 deletions(-) create mode 100644 app/projects/migrations/0093_auto_20220810_1356.py diff --git a/app/projects/api.py b/app/projects/api.py index f306bbff..bdb67dc7 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -423,10 +423,6 @@ def upload_challengetasksubmission_file(request): logger.warning(f"[{project_key}][{request.user.email}] No Access") return HttpResponse("You do not have access to upload this file.", status=403) - if filename.split(".")[-1] != "zip": - logger.error('Not a zip file.') - return HttpResponse("Only .zip files are accepted", status=400) - try: task = ChallengeTask.objects.get(id=task_id) except exceptions.ObjectDoesNotExist: @@ -505,7 +501,8 @@ def upload_challengetasksubmission_file(request): participant=participant, uuid=data['uuid'], location=data['location'], - submission_info=submission_info_json + submission_info=submission_info_json, + file_type=task.submission_file_type, ) # Send an email notification to the submitters. diff --git a/app/projects/migrations/0093_auto_20220810_1356.py b/app/projects/migrations/0093_auto_20220810_1356.py new file mode 100644 index 00000000..6f89090e --- /dev/null +++ b/app/projects/migrations/0093_auto_20220810_1356.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.28 on 2022-08-10 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0092_auto_20220517_1520'), + ] + + operations = [ + migrations.AddField( + model_name='challengetask', + name='submission_file_type', + field=models.CharField(choices=[('zip', 'ZIP'), ('pdf', 'PDF')], default='zip', max_length=15), + ), + migrations.AddField( + model_name='challengetasksubmission', + name='file_type', + field=models.CharField(choices=[('zip', 'ZIP'), ('pdf', 'PDF')], default='zip', max_length=15), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 5f84f060..3839f4cb 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -42,6 +42,18 @@ (AGREEMENT_FORM_TYPE_FILE, 'FILE'), ) +FILE_TYPE_ZIP = "zip" +FILE_TYPE_PDF = "pdf" + +FILES_TYPES = ( + (FILE_TYPE_ZIP, "ZIP"), + (FILE_TYPE_PDF, "PDF"), +) + +FILES_CONTENT_TYPES = { + FILE_TYPE_ZIP: "application/zip", + FILE_TYPE_PDF: "application/pdf", +} def get_agreement_form_upload_path(instance, filename): @@ -567,6 +579,9 @@ class ChallengeTask(models.Model): # Should supervisors be notified of submissions of this task notify_supervisors_of_submissions = models.BooleanField(default=False, blank=False, null=False, help_text="Sends a notification to any emails listed in the project's supervisors field.") + # The content type to restrict file uploads to + submission_file_type = models.CharField(max_length=15, default=FILE_TYPE_ZIP, choices=FILES_TYPES) + def __str__(self): return '%s: %s' % (self.data_project.project_key, self.title) @@ -574,6 +589,10 @@ def clean(self): if self.opened_time is not None and self.closed_time is not None and (self.opened_time > self.closed_time or self.closed_time < self.opened_time): raise ValidationError("Closed time must be a datetime after opened time") + @property + def submission_file_content_type(self): + return FILES_CONTENT_TYPES[self.submission_file_type] + class ChallengeTaskSubmission(models.Model): """ @@ -590,6 +609,7 @@ class ChallengeTaskSubmission(models.Model): location = models.CharField(max_length=12, default=None, blank=True, null=True) submission_info = models.TextField(default=None, blank=True, null=True) deleted = models.BooleanField(default=False) + file_type = models.CharField(max_length=15, default=FILE_TYPE_ZIP, choices=FILES_TYPES) def __str__(self): return '%s' % (self.uuid) diff --git a/app/templates/projects/participate/complete-tasks.html b/app/templates/projects/participate/complete-tasks.html index 2ae5b809..675d64db 100644 --- a/app/templates/projects/participate/complete-tasks.html +++ b/app/templates/projects/participate/complete-tasks.html @@ -7,8 +7,8 @@ {% if task_enabled %}

    - Task: {{ task_detail.task.title }} - + Task: {{ task_detail.task.title }} + {% if task_detail.submissions_left is not None %} {{ task_detail.submissions_left }} submissions left {% endif %} @@ -32,24 +32,31 @@

    You have used up all of your available submissions. You may delete a previous submission if you wish to submit a new one.

    {% endif %} {% else %} -
    + {% if task_detail.task.submission_form_file_path %}
    {{ task_detail.task.submission_form_file_path|get_html_form_file_contents | safe }}
    {% endif %} - + +
    + +
    - - - - - - + +
    +
    + +
    +
    + +
    +
    +
    {% csrf_token %} @@ -63,7 +70,7 @@

    {% for submission in task_detail.submissions %}
  • {{ submission.participant.user.email }} on {{ submission.upload_date|timezone:"America/New_York" }} (EST) - + {# Hide the submission metadata json here #} @@ -94,7 +101,7 @@ - +
  • @@ -120,88 +127,86 @@ var input = $(this); var fileName = input.val().replace(/\\/g, '/').replace(/.*\//, ''); - $(this).parent().parent().find('.file-upload-submit').text('Submit ' + fileName); - $(this).parent().parent().find('.file-upload-browse').hide(); - $(this).parent().parent().find('.file-upload-submit').show(); - - $(this).parent().parent().find('.file-upload-filename').val(fileName); + $('#file-upload-filename').val(fileName); }); // Set the handler for the participant submission form. $(document).on('submit', '.participant-submission-form', function (event) { - // Get the file. - var file = $(this).find(".file-upload-file").prop('files')[0]; - if (file == null) { - error('A file has not been selected, please try again'); - return false; - } - - // Only allow zip files for upload. - if (file.name.split('.').pop() != 'zip') { - error('Only .zip files are accepted.'); - return false; - } - - // Get the form data. - var form = objectifyForm($(this).serializeArray()); - - // Remove the file from the serialized form as it will not be needed yet. - delete form['file']; - - // Add info about the file. - form['content_type'] = file.content; - - // Disable the form and the buttons. - $(this).find(".file-upload-submit").button('loading'); - - $.ajax({ - method: "POST", - data: form, - url: "{% url 'projects:upload_challengetasksubmission_file' %}", - success: function (data, textStatus, jqXHR) { - console.log("submit.success: " + textStatus); - upload(data["post"], data["file"], file, form, $(this)); - }, - error: function (jqXHR, textStatus, errorThrown) { - console.log("submit.error: " + errorThrown); - error('Something happened, please try again'); - } - }); - - // Return false to prevent a form submit effect - return false; + // Get the form + var form = $(this); + + // Get the file input + var fileInput = form.find(".file-upload-file"); + + // Get the file. + var file = fileInput.prop('files')[0]; + if (file == null) { + error('A file has not been selected, please try again', form); + return false; + } + + // Validate content type + if( file.type !== fileInput.data("content-type")) { + console.log(`Blob type ${file.type} !== ${fileInput.data("content-type")}`); + error(`Only files of type "${fileInput.data("content-type")}" are accepted`, form); + return true; + } + + // Get the form data. + var formData = objectifyForm($(form).serializeArray()); + + console.log(formData); + + // Remove the file from the serialized form as it will not be needed yet. + delete formData['file']; + + // Add info about the file. + formData['content_type'] = file.content; + + // Disable the form and the buttons. + form.find(".file-upload-submit").button('loading'); + + $.ajax({ + method: "POST", + data: formData, + url: "{% url 'projects:upload_challengetasksubmission_file' %}", + success: function (data, textStatus, jqXHR) { + console.log("submit.success: " + textStatus); + upload(data["post"], data["file"], file, formData, form); + }, + error: function (jqXHR, textStatus, errorThrown) { + console.log("submit.error: " + errorThrown); + error('Something happened, please try again', form); + } + }); + + // Return false to prevent a form submit effect + return false; }); // Uploads the file to AWS S3. - function upload(post, fileRecord, file, submission_form, form_element) { + function upload(post, fileRecord, file, formData, form) { console.log('upload: ' + fileRecord["filename"]); // Construct the needed data using the policy for AWS - var form = new FormData(); + var uploadForm = new FormData(); var fields = post["fields"]; for(var key in fields) { - form.append(key, fields[key]); + uploadForm.append(key, fields[key]); } // Add the file - form.append('file', file); + uploadForm.append('file', file); // Send the file to S3. $.ajax({ url: post["url"], datatype: 'xml', - data: form, + data: uploadForm, type: 'POST', contentType: false, processData: false, - xhr: function () { - - // Add a progress handler - var xhr = $.ajaxSettings.xhr(); - xhr.upload.onprogress = progress; - return xhr; - }, success: function (data, textStatus, jqXHR) { console.log('upload.success: ' + textStatus); @@ -209,7 +214,7 @@ // $("#participant-submission-form :input").prop("disabled", true); // Inform the server that the upload completed. - complete(fileRecord, submission_form, form_element); + complete(fileRecord, formData, form); }, error: function (jqXHR, textStatus, errorThrown) { console.log('upload.error: ' + errorThrown + ', ' + textStatus); @@ -217,20 +222,20 @@ // Check if aborted. if( !jqXHR.getAllResponseHeaders() ) { // Cancelled notification. - aborted(form_element); + aborted(form); } else { - error('Something happened, please try again', form_element); + error('Something happened, please try again', form); } } }); } - function complete(fileRecord, submission_form, form_element) { + function complete(fileRecord, formData, form) { console.log('complete: ' + fileRecord["filename"]); - - // Combine information about the fileservice metadata on the file with the + + // Combine information about the fileservice metadata on the file with the // original form submission. - $.extend(fileRecord, submission_form) + $.extend(fileRecord, formData) // Inform the server that the upload completed. $.ajax({ @@ -241,7 +246,7 @@ console.log("complete.success: " + textStatus); // Notify. - success(form_element); + success(form); // Refresh the page. setTimeout(function(){ @@ -252,35 +257,36 @@ console.log("complete.error: " + errorThrown); // Error with message. - error('Something happened, please try again', form_element); + error('Something happened, please try again', form); } }); } - function progress(event) { - // Ensure it can be used - if (event.lengthComputable) { - var progress = event.loaded / event.total * 100; - console.log('upload.progress: ' + progress); - } - } - - function error(message, form_element) { - form_element.find('.file-upload-submit').text('File failed to upload'); + function error(message, form) { notify('danger', message, 'glyphicon glyphicon-remove'); + + // Disable the form and the buttons. + form.find(".file-upload-submit").button('reset'); } - function aborted(form_element) { - form_element.find('.file-upload-submit').text('File uploaded canceled'); + function aborted(form) { + form.find('.file-upload-submit').text('File uploaded canceled'); notify('warning', 'The upload was cancelled', 'glyphicon glyphicon-warning-sign'); } - function success(form_element) { - // Add a little delay - setTimeout(function(){ - form_element.find('.file-upload-submit').text('File uploaded successfully!'); - notify('success', 'The upload has completed successfully! Refreshing page.', 'glyphicon glyphicon-ok'); - }, 500); + function success(form) { + + // Update the text. + $('#file-upload-progress').text('Completed!'); + + // Add a little delay + setTimeout(function(){ + notify('success', 'The upload has completed successfully! Refreshing page.', 'glyphicon glyphicon-ok'); + }, 500); + + // Disable the form and the buttons. + form.find(".file-upload-submit").prop("disabled", true); + form.find(".file-upload-submit").text('File uploaded successfully'); } }); @@ -292,7 +298,7 @@ // Grab the information from an adjacent div var submission_info = $(this).next(".submission-info-details").text(); - + // Parse the string into an actual json var submission_info_json = JSON.parse(submission_info); var submission_info_json_prettyprint = JSON.stringify(submission_info_json, null, 4); @@ -341,4 +347,4 @@ } }); }); - \ No newline at end of file + From f194a7c8f65752ea637e22ee329d91e2c5410b0a Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 11 Aug 2022 09:18:57 -0600 Subject: [PATCH 546/613] HYP-HOTFIX-081122 - Dropped django-storages to < 1.13 due to support dropped for Django < 3.2 --- requirements.in | 2 +- requirements.txt | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/requirements.in b/requirements.in index fb8f39e8..2737a1fa 100644 --- a/requirements.in +++ b/requirements.in @@ -10,7 +10,7 @@ django-health-check<4.0 django-jquery<4.0 django-jsonfield-backport<2.0 django-picklefield<3.1 -django-storages<2.0 +django-storages<1.13 django-stronghold<=1.0 djangorestframework<4.0 django-smtp-ssl<2.0 diff --git a/requirements.txt b/requirements.txt index 94b14692..ddcc8132 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,21 +8,21 @@ arrow==1.2.2 \ --hash=sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b \ --hash=sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177 # via django-q -awscli==1.25.37 \ - --hash=sha256:43e6062245ffcbc3b2903c7c5255ab573eea29f5f14be9c1b20e433f7b338a51 \ - --hash=sha256:878b985f1587fa69bbb2193c138c9851b5f0369c9a140b1a33e6a519d1a0a876 +awscli==1.25.49 \ + --hash=sha256:0223f8ad26e22121e19cb959f35d300e9777ff207c0633db7cb2dfa578d6fcad \ + --hash=sha256:66f0a07a341d58a3849effcf258f4f241bcfb962c647e33de66673f2109eb59c # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.37 \ - --hash=sha256:801c64aa3dbeabbf2e06e27bc3530245ebc6b14b6d7e8bcff08ff67ec6e60c88 \ - --hash=sha256:c5f0b44a77d01d6f714f91f181b1e0819fed9b99423234cd4c2cce6704e915b3 +boto3==1.24.49 \ + --hash=sha256:19ce58ed29d8b9dc892c06978c0097e7149641511c6b41142ecfee0a5ed19b5b \ + --hash=sha256:c960712b3a833d321a997a9fb59f3da21b026d5a23727c9cb49866d3794cdb67 # via -r requirements.in -botocore==1.27.37 \ - --hash=sha256:4616a7bb869b890c4b582460423e04447dd24b4af76ac761ec68236b6cb0ef7a \ - --hash=sha256:c73988eff91897fe840ae6a9b459705e3086bc7f62f37a12999a7f4564002f63 +botocore==1.27.49 \ + --hash=sha256:8e9556ef9f7f492e0755437a1430e32f63ddaf6df61c105957805067b5e22dbb \ + --hash=sha256:ac491c54cdf4126c98a999c5d473d734dc91c5b1e003d0fdafd5007d1408d046 # via # awscli # boto3 @@ -167,8 +167,9 @@ django-countries==6.1.3 \ --hash=sha256:64015977a5989bcb0e645007299b19fe8ac117466af375161b26bcfa32ae2808 \ --hash=sha256:a0f77154ae08cb38a0d65530a399ead5f5837ebf6c74f7576e71bb7acdacca94 # via -r requirements.in -django-dbmi-client==0.5.1 \ - --hash=sha256:d2b64316e6291d8941417f8130c8f77fd97460a20180aff2f9a20b7683f75ec8 +django-dbmi-client==0.5.2 \ + --hash=sha256:270fff5b6f089858b7bb6f4c3fe64c8d616348146feb5b8c6c1f158f1dab144d \ + --hash=sha256:4dbd944c7fd5d4f13ce073facb761df175057d24ff4e28153a8a97e38c5d93cd # via -r requirements.in django-health-check==3.16.5 \ --hash=sha256:1edfd49293ccebbce29f9da609c407f307aee240ab799ab4201031341ae78c0f \ From 0774fc17bf91ba7c0199e9c08b1f60ee8d12013f Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 9 Aug 2022 12:25:37 -0600 Subject: [PATCH 547/613] HYP-288 - Added internal agreement forms for uploading and use by administrators only --- app/manage/forms.py | 18 ++- app/manage/urls.py | 2 + app/manage/views.py | 111 +++++++++++++++++- .../migrations/0093_agreementform_internal.py | 18 +++ app/projects/models.py | 1 + app/templates/manage/team.html | 12 ++ .../manage/upload-signed-agreement-form.html | 20 ++++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 app/projects/migrations/0093_agreementform_internal.py create mode 100644 app/templates/manage/upload-signed-agreement-form.html diff --git a/app/manage/forms.py b/app/manage/forms.py index f2f46ae4..4d142c45 100644 --- a/app/manage/forms.py +++ b/app/manage/forms.py @@ -3,9 +3,10 @@ from bootstrap_datepicker_plus import DateTimePickerInput from dal import autocomplete -from projects.models import DataProject +from projects.models import AgreementForm, DataProject from projects.models import HostedFile from projects.models import Team +from projects.models import AGREEMENT_FORM_TYPE_FILE # TODO Convert all other manual forms into Django forms # ... @@ -63,3 +64,18 @@ class NotificationForm(forms.Form): project = forms.ModelChoiceField(queryset=DataProject.objects.all(), widget=forms.HiddenInput) message = forms.CharField(label='Message', required=True, widget=forms.Textarea) team = forms.ModelChoiceField(queryset=Team.objects.all(), widget=forms.HiddenInput) + + +class UploadSignedAgreementFormForm(forms.Form): + agreement_form = forms.ModelChoiceField(queryset=AgreementForm.objects.filter(type=AGREEMENT_FORM_TYPE_FILE, internal=True), widget=forms.Select(attrs={'class': 'form-control'})) + project_key = forms.CharField(label='Project Key', max_length=128, required=True, widget=forms.HiddenInput()) + participant = forms.CharField(label='Participant', max_length=128, required=True, widget=forms.HiddenInput()) + signed_agreement_form = forms.FileField(label="Signed Agreement Form PDF", required=True) + + def __init__(self, *args, **kwargs): + project_key = kwargs.pop('project_key', None) + super(UploadSignedAgreementFormForm, self).__init__(*args, **kwargs) + + # Limit agreement form choices to those related to the passed project + if project_key: + self.fields['agreement_form'].queryset = DataProject.objects.get(project_key=project_key).agreement_forms.all() diff --git a/app/manage/urls.py b/app/manage/urls.py index 4cbec266..fae3d753 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -6,6 +6,7 @@ from manage.views import manage_team from manage.views import ProjectParticipants from manage.views import team_notification +from manage.views import UploadSignedAgreementFormView from manage.api import set_dataproject_details from manage.api import set_dataproject_registration_status @@ -58,6 +59,7 @@ url(r'^grant-view-permission/(?P[^/]+)/(?P[^/]+)/$', grant_view_permission, name='grant-view-permission'), url(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), url(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), + url(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), url(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), url(r'^(?P[^/]+)/(?P[^/]+)/$', manage_team, name='manage-team'), ] diff --git a/app/manage/views.py b/app/manage/views.py index b292f85c..f196b56c 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -1,5 +1,5 @@ import logging - +from datetime import datetime from hypatio.auth0authenticate import user_auth_and_jwt from django.conf import settings @@ -25,6 +25,7 @@ from manage.forms import NotificationForm from manage.models import ChallengeTaskSubmissionExport +from manage.forms import UploadSignedAgreementFormForm from projects.models import AgreementForm, ChallengeTaskSubmission from projects.models import DataProject from projects.models import Participant @@ -33,6 +34,7 @@ from projects.models import SignedAgreementForm from projects.models import HostedFile from projects.models import HostedFileDownload +from projects.models import SIGNED_FORM_APPROVED # Get an instance of a logger logger = logging.getLogger(__name__) @@ -604,6 +606,15 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht team_accepted_forms += 1 signed_accepted_agreement_forms += 1 + # Add internal signed agreement forms + for signed_agreement_form in SignedAgreementForm.objects.filter( + agreement_form__internal=True, + user__email=email, + project=project): + + # Add it + signed_agreement_forms.append(signed_agreement_form) + team_member_details.append({ 'email': email, 'user_info': user_info, @@ -643,3 +654,101 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht } return render(request, template_name, context=context) + + +@method_decorator([user_auth_and_jwt], name='dispatch') +class UploadSignedAgreementFormView(View): + """ + View to upload signed agreement forms for participants. + + * Requires token authentication. + * Only admin users are able to access this view. + """ + def get(self, request, project_key, user_email, *args, **kwargs): + """ + Return the upload form template + """ + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format( + email=user.email, + project_key=project_key + )) + return HttpResponse(403) + + # Return file upload form + form = UploadSignedAgreementFormForm(initial={ + "project_key": project_key, + "participant": user_email, + }) + + # Set context + context = { + "form": form, + "project_key": project_key, + "user_email": user_email, + } + + # Render html + return render(request, "manage/upload-signed-agreement-form.html", context) + + def post(self, request, project_key, user_email, *args, **kwargs): + """ + Process the form + """ + user = request.user + user_jwt = request.COOKIES.get("DBMI_JWT", None) + + sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + is_manager = sciauthz.user_has_manage_permission(project_key) + + if not is_manager: + logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format( + email=user.email, + project_key=project_key + )) + return HttpResponse(403) + + # Assembles the form and run validation. + form = UploadSignedAgreementFormForm(data=request.POST, files=request.FILES) + if not form.is_valid(): + logger.warning('Form failed: {}'.format(form.errors.as_json())) + return HttpResponse(status=400) + + logger.debug(f"[upload_signed_agreement_form] Data -> {form.cleaned_data}") + + signed_agreement_form = form.cleaned_data['signed_agreement_form'] + agreement_form = form.cleaned_data['agreement_form'] + project_key = form.cleaned_data['project_key'] + participant_email = form.cleaned_data['participant'] + + project = DataProject.objects.get(project_key=project_key) + participant = Participant.objects.get(project=project, user__email=participant_email) + + signed_agreement_form = SignedAgreementForm( + user=participant.user, + agreement_form=agreement_form, + project=project, + date_signed=datetime.now(), + upload=signed_agreement_form, + status=SIGNED_FORM_APPROVED, + ) + signed_agreement_form.save() + + # Create the response. + response = HttpResponse(status=201) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "success", "Signed agreement form successfully uploaded", "thumbs-up" + ) + + # Close the modal + response['X-IC-Script'] += "$('#page-modal').modal('hide');" + + return response diff --git a/app/projects/migrations/0093_agreementform_internal.py b/app/projects/migrations/0093_agreementform_internal.py new file mode 100644 index 00000000..acfe5fe1 --- /dev/null +++ b/app/projects/migrations/0093_agreementform_internal.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2022-08-09 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0092_auto_20220517_1520'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='internal', + field=models.BooleanField(default=False, help_text='Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants'), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 3839f4cb..b60bc70c 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -108,6 +108,7 @@ class AgreementForm(models.Model): type = models.CharField(max_length=50, choices=AGREEMENT_FORM_TYPE, blank=True, null=True) order = models.IntegerField(default=50, help_text="Indicate an order (lowest number = first listing) for how the Agreement Forms should be listed during registration workflows.") content = models.TextField(blank=True, null=True, help_text="If Agreement Form type is set to 'MODEL', the HTML set here will be rendered for the user") + internal = models.BooleanField(default=False, help_text="Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants") def __str__(self): return '%s' % (self.name) diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index 80ccaa7d..2602fe85 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -197,6 +197,10 @@

    Team members

    {{ form.agreement_form.short_name }} {% endfor %} + +
    {% if member.access_granted %} @@ -527,4 +531,12 @@

    Previous comments:

    }); + +{# Add a placeholder for any modal dialogs #} + {% endblock %} diff --git a/app/templates/manage/upload-signed-agreement-form.html b/app/templates/manage/upload-signed-agreement-form.html new file mode 100644 index 00000000..795486a1 --- /dev/null +++ b/app/templates/manage/upload-signed-agreement-form.html @@ -0,0 +1,20 @@ +{% load bootstrap3 %} + + + + + + From fa2def21c3b9e99604c55858abfa13aab780df76 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 11 Aug 2022 10:05:38 -0600 Subject: [PATCH 548/613] HYP-286 - Renamed migration for dependency resolution --- ...reementform_internal.py => 0094_agreementform_internal.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename app/projects/migrations/{0093_agreementform_internal.py => 0094_agreementform_internal.py} (81%) diff --git a/app/projects/migrations/0093_agreementform_internal.py b/app/projects/migrations/0094_agreementform_internal.py similarity index 81% rename from app/projects/migrations/0093_agreementform_internal.py rename to app/projects/migrations/0094_agreementform_internal.py index acfe5fe1..72872214 100644 --- a/app/projects/migrations/0093_agreementform_internal.py +++ b/app/projects/migrations/0094_agreementform_internal.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.28 on 2022-08-09 17:57 +# Generated by Django 2.2.28 on 2022-08-11 17:57 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('projects', '0092_auto_20220517_1520'), + ('projects', '0093_auto_20220810_1356'), ] operations = [ From 6eba10d3c2106f0a96da4bd9d7a8165abccbcd43 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 29 Sep 2022 09:08:40 -0600 Subject: [PATCH 549/613] DBMSIVC-122 - Upgraded to Django 4.x --- app/contact/urls.py | 6 +- app/hypatio/urls.py | 24 +-- app/manage/apps.py | 1 + app/manage/urls.py | 58 +++--- app/profile/urls.py | 12 +- app/projects/apps.py | 1 + .../0088_signedagreementform_fields.py | 4 +- ...form_id_alter_challengetask_id_and_more.py | 103 ++++++++++ app/projects/models.py | 2 +- app/projects/urls.py | 36 ++-- requirements.in | 10 +- requirements.txt | 180 +++++++++--------- 12 files changed, 276 insertions(+), 161 deletions(-) create mode 100644 app/projects/migrations/0095_alter_agreementform_id_alter_challengetask_id_and_more.py diff --git a/app/contact/urls.py b/app/contact/urls.py index e9f6c240..36579742 100644 --- a/app/contact/urls.py +++ b/app/contact/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import url +from django.urls import re_path from .views import contact_form app_name = 'contact' urlpatterns = ( - url(r'^(?P[^/]+)/$', contact_form, name='contact_form'), - url(r'^', contact_form, name='contact_form'), + re_path(r'^(?P[^/]+)/$', contact_form, name='contact_form'), + re_path(r'^', contact_form, name='contact_form'), ) diff --git a/app/hypatio/urls.py b/app/hypatio/urls.py index 7b03fbfe..28b9ffef 100644 --- a/app/hypatio/urls.py +++ b/app/hypatio/urls.py @@ -1,5 +1,5 @@ -from django.conf.urls import include -from django.conf.urls import url +from django.urls import include +from django.urls import re_path from django.contrib import admin from hypatio.views import index @@ -9,14 +9,14 @@ urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^contact/', include('contact.urls', namespace='contact')), - url(r'^manage/', include('manage.urls', namespace='manage')), - url(r'^projects/', include('projects.urls', namespace='projects')), - url(r'^profile/', include('profile.urls', namespace='profile')), - url(r'^data-sets/$', list_data_projects, name='data-sets'), - url(r'^data-challenges/$', list_data_challenges, name='data-challenges'), - url(r'^software-projects/$', list_software_projects, name='software-projects'), - url(r'^healthcheck/?', include('health_check.urls')), - url(r'^', index, name='index'), + re_path(r'^admin/', admin.site.urls), + re_path(r'^contact/', include('contact.urls', namespace='contact')), + re_path(r'^manage/', include('manage.urls', namespace='manage')), + re_path(r'^projects/', include('projects.urls', namespace='projects')), + re_path(r'^profile/', include('profile.urls', namespace='profile')), + re_path(r'^data-sets/$', list_data_projects, name='data-sets'), + re_path(r'^data-challenges/$', list_data_challenges, name='data-challenges'), + re_path(r'^software-projects/$', list_software_projects, name='software-projects'), + re_path(r'^healthcheck/?', include('health_check.urls')), + re_path(r'^', index, name='index'), ] diff --git a/app/manage/apps.py b/app/manage/apps.py index 5a55842e..f935cb63 100644 --- a/app/manage/apps.py +++ b/app/manage/apps.py @@ -3,3 +3,4 @@ class ManageConfig(AppConfig): name = 'manage' + default_auto_field = 'django.db.models.BigAutoField' diff --git a/app/manage/urls.py b/app/manage/urls.py index fae3d753..174f1d1d 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from manage.apps import ManageConfig from manage.views import DataProjectListManageView @@ -34,32 +34,32 @@ app_name = ManageConfig.name urlpatterns = [ - url(r'^$', DataProjectListManageView.as_view(), name='manage-projects'), - url(r'^download-email-list/$', download_email_list, name='download-email-list'), - url(r'^set-dataproject-details/$', set_dataproject_details, name='set-dataproject-details'), - url(r'^set-dataproject-registration-status/$', set_dataproject_registration_status, name='set-dataproject-registration-status'), - url(r'^set-dataproject-visible-status/$', set_dataproject_visible_status, name='set-dataproject-visible-status'), - url(r'^get-static-agreement-form-html/$', get_static_agreement_form_html, name='get-static-agreement-form-html'), - url(r'^get-hosted-file-edit-form/$', get_hosted_file_edit_form, name='get-hosted-file-edit-form'), - url(r'^get-hosted-file-logs/$', get_hosted_file_logs, name='get-hosted-file-logs'), - url(r'^process-hosted-file-edit-form-submission/$', process_hosted_file_edit_form_submission, name='process-hosted-file-edit-form-submission'), - url(r'^download-signed-form/$', download_signed_form, name='download-signed-form'), - url(r'^get-signed-form-status/$', get_signed_form_status, name='get-signed-form-status'), - url(r'^change-signed-form-status/$', change_signed_form_status, name='change-signed-form-status'), - url(r'^save-team-comment/$', save_team_comment, name='save-team-comment'), - url(r'^set-team-status/$', set_team_status, name='set-team-status'), - url(r'^delete-team/$', delete_team, name='delete-team'), - url(r'^team-notification/$', team_notification, name='team-notification'), - url(r'^download-team-submissions/(?P[^/]+)/(?P[^/]+)/$', download_team_submissions, name='download-team-submissions'), - url(r'^download-submission/(?P[^/]+)/$', download_submission, name='download-submission'), - url(r'^export-submissions/(?P[^/]+)/$', export_submissions, name='export-submissions'), - url(r'^download-submissions-export/(?P[^/]+)/(?P[^/]+)/$', download_submissions_export, name='download-submissions-export'), - url(r'^host-submission/(?P[^/]+)/$', host_submission, name='host-submission'), - url(r'^sync-view-permissions/(?P[^/]+)/$', sync_view_permissions, name='sync-view-permissions'), - url(r'^grant-view-permission/(?P[^/]+)/(?P[^/]+)/$', grant_view_permission, name='grant-view-permission'), - url(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), - url(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), - url(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), - url(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), - url(r'^(?P[^/]+)/(?P[^/]+)/$', manage_team, name='manage-team'), + re_path(r'^$', DataProjectListManageView.as_view(), name='manage-projects'), + re_path(r'^download-email-list/$', download_email_list, name='download-email-list'), + re_path(r'^set-dataproject-details/$', set_dataproject_details, name='set-dataproject-details'), + re_path(r'^set-dataproject-registration-status/$', set_dataproject_registration_status, name='set-dataproject-registration-status'), + re_path(r'^set-dataproject-visible-status/$', set_dataproject_visible_status, name='set-dataproject-visible-status'), + re_path(r'^get-static-agreement-form-html/$', get_static_agreement_form_html, name='get-static-agreement-form-html'), + re_path(r'^get-hosted-file-edit-form/$', get_hosted_file_edit_form, name='get-hosted-file-edit-form'), + re_path(r'^get-hosted-file-logs/$', get_hosted_file_logs, name='get-hosted-file-logs'), + re_path(r'^process-hosted-file-edit-form-submission/$', process_hosted_file_edit_form_submission, name='process-hosted-file-edit-form-submission'), + re_path(r'^download-signed-form/$', download_signed_form, name='download-signed-form'), + re_path(r'^get-signed-form-status/$', get_signed_form_status, name='get-signed-form-status'), + re_path(r'^change-signed-form-status/$', change_signed_form_status, name='change-signed-form-status'), + re_path(r'^save-team-comment/$', save_team_comment, name='save-team-comment'), + re_path(r'^set-team-status/$', set_team_status, name='set-team-status'), + re_path(r'^delete-team/$', delete_team, name='delete-team'), + re_path(r'^team-notification/$', team_notification, name='team-notification'), + re_path(r'^download-team-submissions/(?P[^/]+)/(?P[^/]+)/$', download_team_submissions, name='download-team-submissions'), + re_path(r'^download-submission/(?P[^/]+)/$', download_submission, name='download-submission'), + re_path(r'^export-submissions/(?P[^/]+)/$', export_submissions, name='export-submissions'), + re_path(r'^download-submissions-export/(?P[^/]+)/(?P[^/]+)/$', download_submissions_export, name='download-submissions-export'), + re_path(r'^host-submission/(?P[^/]+)/$', host_submission, name='host-submission'), + re_path(r'^sync-view-permissions/(?P[^/]+)/$', sync_view_permissions, name='sync-view-permissions'), + re_path(r'^grant-view-permission/(?P[^/]+)/(?P[^/]+)/$', grant_view_permission, name='grant-view-permission'), + re_path(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), + re_path(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), + re_path(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), + re_path(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), + re_path(r'^(?P[^/]+)/(?P[^/]+)/$', manage_team, name='manage-team'), ] diff --git a/app/profile/urls.py b/app/profile/urls.py index f7912942..10968c47 100644 --- a/app/profile/urls.py +++ b/app/profile/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from profile.views import profile from profile.views import send_confirmation_email_view @@ -7,8 +7,8 @@ app_name = 'profile' urlpatterns = [ - url(r'^$', profile, name='profile'), - url(r'^send_confirmation_email/$', send_confirmation_email_view, name='send_confirmation_email'), - url(r'^update/$', update_profile, name='update'), - url(r'^signout/$', signout, name='signout'), -] \ No newline at end of file + re_path(r'^$', profile, name='profile'), + re_path(r'^send_confirmation_email/$', send_confirmation_email_view, name='send_confirmation_email'), + re_path(r'^update/$', update_profile, name='update'), + re_path(r'^signout/$', signout, name='signout'), +] diff --git a/app/projects/apps.py b/app/projects/apps.py index 6c1ff712..8f92c096 100644 --- a/app/projects/apps.py +++ b/app/projects/apps.py @@ -21,6 +21,7 @@ def check_fileservice(sender, **kwargs): class ProjectsConfig(AppConfig): name = 'projects' + default_auto_field = 'django.db.models.BigAutoField' def ready(self): """ diff --git a/app/projects/migrations/0088_signedagreementform_fields.py b/app/projects/migrations/0088_signedagreementform_fields.py index 0fb3c8dd..92e60d0c 100644 --- a/app/projects/migrations/0088_signedagreementform_fields.py +++ b/app/projects/migrations/0088_signedagreementform_fields.py @@ -1,7 +1,7 @@ # Generated by Django 2.2.26 on 2022-01-31 20:35 from django.db import migrations -import django_jsonfield_backport.models +import django.db.models class Migration(migrations.Migration): @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='signedagreementform', name='fields', - field=django_jsonfield_backport.models.JSONField(blank=True, null=True), + field=django.db.models.JSONField(blank=True, null=True), ), ] diff --git a/app/projects/migrations/0095_alter_agreementform_id_alter_challengetask_id_and_more.py b/app/projects/migrations/0095_alter_agreementform_id_alter_challengetask_id_and_more.py new file mode 100644 index 00000000..2034cdb3 --- /dev/null +++ b/app/projects/migrations/0095_alter_agreementform_id_alter_challengetask_id_and_more.py @@ -0,0 +1,103 @@ +# Generated by Django 4.1.1 on 2022-09-29 15:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0094_agreementform_internal'), + ] + + operations = [ + migrations.AlterField( + model_name='agreementform', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='challengetask', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='challengetasksubmissiondownload', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='dataproject', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='duasignedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='hostedfile', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='hostedfiledownload', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='hostedfileset', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='institution', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='mayosignedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='mimic3signedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='nlpduasignedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='nlpwhysignedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='participant', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='rocsignedagreementformfields', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='signedagreementform', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='team', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='teamcomment', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index b60bc70c..e3b8cae7 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -5,7 +5,7 @@ from django.db import models from django.contrib.auth.models import User from django.core.exceptions import ValidationError -from django_jsonfield_backport.models import JSONField +from django.db.models import JSONField from django.core.files.uploadedfile import UploadedFile TEAM_PENDING = 'Pending' diff --git a/app/projects/urls.py b/app/projects/urls.py index b4bcc720..878bfd62 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from projects.apps import ProjectsConfig from projects.views import list_data_projects @@ -23,21 +23,21 @@ app_name = ProjectsConfig.name urlpatterns = [ - url(r'^$', list_data_projects, name='index'), - url(r'^autocomplete/hostedfileset/$', HostedFileSetAutocomplete.as_view(create_field='title'), name='hostedfileset-autocomplete'), - url(r'^submit_user_permission_request/$', submit_user_permission_request, name='submit_user_permission_request'), - url(r'^save_signed_agreement_form', save_signed_agreement_form, name='save_signed_agreement_form'), - url(r'^save_signed_external_agreement_form', save_signed_external_agreement_form, name='save_signed_external_agreement_form'), - url(r'^upload_signed_agreement_form', upload_signed_agreement_form, name='upload_signed_agreement_form'), - url(r'^join_team/$', join_team, name='join_team'), - url(r'^leave_team/$', leave_team, name='leave_team'), - url(r'^approve_team_join/$', approve_team_join, name='approve_team_join'), - url(r'^reject_team_join/$', reject_team_join, name='reject_team_join'), - url(r'^create_team/$', create_team, name='create_team'), - url(r'^finalize_team/$', finalize_team, name='finalize_team'), - url(r'^signed_agreement_form/$', signed_agreement_form, name='signed_agreement_form'), - url(r'^download_dataset/$', download_dataset, name='download_dataset'), - url(r'^upload_challengetasksubmission_file/$', upload_challengetasksubmission_file, name="upload_challengetasksubmission_file"), - url(r'^delete_challengetasksubmission/$', delete_challengetasksubmission, name='delete_challengetasksubmission'), - url(r'^(?P[^/]+)/$', DataProjectView.as_view(), name="view-project"), + re_path(r'^$', list_data_projects, name='index'), + re_path(r'^autocomplete/hostedfileset/$', HostedFileSetAutocomplete.as_view(create_field='title'), name='hostedfileset-autocomplete'), + re_path(r'^submit_user_permission_request/$', submit_user_permission_request, name='submit_user_permission_request'), + re_path(r'^save_signed_agreement_form', save_signed_agreement_form, name='save_signed_agreement_form'), + re_path(r'^save_signed_external_agreement_form', save_signed_external_agreement_form, name='save_signed_external_agreement_form'), + re_path(r'^upload_signed_agreement_form', upload_signed_agreement_form, name='upload_signed_agreement_form'), + re_path(r'^join_team/$', join_team, name='join_team'), + re_path(r'^leave_team/$', leave_team, name='leave_team'), + re_path(r'^approve_team_join/$', approve_team_join, name='approve_team_join'), + re_path(r'^reject_team_join/$', reject_team_join, name='reject_team_join'), + re_path(r'^create_team/$', create_team, name='create_team'), + re_path(r'^finalize_team/$', finalize_team, name='finalize_team'), + re_path(r'^signed_agreement_form/$', signed_agreement_form, name='signed_agreement_form'), + re_path(r'^download_dataset/$', download_dataset, name='download_dataset'), + re_path(r'^upload_challengetasksubmission_file/$', upload_challengetasksubmission_file, name="upload_challengetasksubmission_file"), + re_path(r'^delete_challengetasksubmission/$', delete_challengetasksubmission, name='delete_challengetasksubmission'), + re_path(r'^(?P[^/]+)/$', DataProjectView.as_view(), name="view-project"), ] diff --git a/requirements.in b/requirements.in index 2737a1fa..3eb75790 100644 --- a/requirements.in +++ b/requirements.in @@ -1,16 +1,16 @@ awscli<2.0 boto3<2.0 -Django<3.0 +Django<5.0 django-autocomplete-light<4.0 -django-bootstrap3<16.0 +django-bootstrap3<23.0 django-bootstrap-datepicker-plus<4.0 -django-countries<7.0 +django-countries<8.0 django-dbmi-client<2.0 django-health-check<4.0 django-jquery<4.0 django-jsonfield-backport<2.0 -django-picklefield<3.1 -django-storages<1.13 +django-picklefield<4 +django-storages<2.0 django-stronghold<=1.0 djangorestframework<4.0 django-smtp-ssl<2.0 diff --git a/requirements.txt b/requirements.txt index ddcc8132..db18352e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,32 +4,36 @@ # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in # -arrow==1.2.2 \ - --hash=sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b \ - --hash=sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177 +arrow==1.2.3 \ + --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ + --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via django-q -awscli==1.25.49 \ - --hash=sha256:0223f8ad26e22121e19cb959f35d300e9777ff207c0633db7cb2dfa578d6fcad \ - --hash=sha256:66f0a07a341d58a3849effcf258f4f241bcfb962c647e33de66673f2109eb59c +asgiref==3.5.2 \ + --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ + --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 + # via django +awscli==1.25.83 \ + --hash=sha256:1269504799e81d05398248ab34c0c3a39a8b4a96e551bf60fcc9932962eed463 \ + --hash=sha256:73acd376a4ebfa97fe6d8bf2a012de37410685f666f5cebce9fa60980d145737 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.49 \ - --hash=sha256:19ce58ed29d8b9dc892c06978c0097e7149641511c6b41142ecfee0a5ed19b5b \ - --hash=sha256:c960712b3a833d321a997a9fb59f3da21b026d5a23727c9cb49866d3794cdb67 +boto3==1.24.82 \ + --hash=sha256:2a9b283cd4208cfcea873baac29e5ceb011afbd5a5c5b7a35cb305f377c39f60 \ + --hash=sha256:90d590eda672775ef5ad0cdd4f5c2a51a964e3de631f3deccbc96e29cf0cd5a9 # via -r requirements.in -botocore==1.27.49 \ - --hash=sha256:8e9556ef9f7f492e0755437a1430e32f63ddaf6df61c105957805067b5e22dbb \ - --hash=sha256:ac491c54cdf4126c98a999c5d473d734dc91c5b1e003d0fdafd5007d1408d046 +botocore==1.27.82 \ + --hash=sha256:034cdcfb74bcca9124fcaafed857ad78ec572ff95ed802b0608fa7505aaf4a1f \ + --hash=sha256:b122c7048a79fef53f3d45f7ca2999c2559e15fc4531653824ebd3154d77ede5 # via # awscli # boto3 # s3transfer -certifi==2022.6.15 \ - --hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \ - --hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412 +certifi==2022.9.24 \ + --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \ + --hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -97,37 +101,41 @@ cffi==1.15.1 \ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 # via cryptography -charset-normalizer==2.1.0 \ - --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \ - --hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413 +charset-normalizer==2.1.1 \ + --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ + --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f # via requests colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==37.0.4 \ - --hash=sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59 \ - --hash=sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596 \ - --hash=sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3 \ - --hash=sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5 \ - --hash=sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab \ - --hash=sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884 \ - --hash=sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82 \ - --hash=sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b \ - --hash=sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441 \ - --hash=sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa \ - --hash=sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d \ - --hash=sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b \ - --hash=sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a \ - --hash=sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6 \ - --hash=sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157 \ - --hash=sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280 \ - --hash=sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282 \ - --hash=sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67 \ - --hash=sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8 \ - --hash=sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046 \ - --hash=sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327 \ - --hash=sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9 +cryptography==38.0.1 \ + --hash=sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a \ + --hash=sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f \ + --hash=sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0 \ + --hash=sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407 \ + --hash=sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7 \ + --hash=sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6 \ + --hash=sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153 \ + --hash=sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750 \ + --hash=sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad \ + --hash=sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6 \ + --hash=sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b \ + --hash=sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5 \ + --hash=sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a \ + --hash=sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d \ + --hash=sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d \ + --hash=sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294 \ + --hash=sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0 \ + --hash=sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a \ + --hash=sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac \ + --hash=sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61 \ + --hash=sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013 \ + --hash=sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e \ + --hash=sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb \ + --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ + --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ + --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 # via # django-dbmi-client # jwcrypto @@ -135,9 +143,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==2.2.28 \ - --hash=sha256:0200b657afbf1bc08003845ddda053c7641b9b24951e52acd51f6abda33a7413 \ - --hash=sha256:365429d07c1336eb42ba15aa79f45e1c13a0b04d5c21569e7d596696418a6a45 +django==4.1.1 \ + --hash=sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e \ + --hash=sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -157,23 +165,23 @@ django-bootstrap-datepicker-plus==3.0.5 \ --hash=sha256:490058eba99d47f48a7d24fa78581c0e36375bdc7aa9605783eeb170d51fd0df \ --hash=sha256:a8bc19cc6846f97ff1e6c447f4e0387881d16e8afa1e8bd7a652c19e545c566b # via -r requirements.in -django-bootstrap3==15.0.0 \ - --hash=sha256:601c918a466e6a702f7b394a94826ba9f53c9cc879ccfc26579e3473eef80f53 \ - --hash=sha256:f803efd5605046b8f467523dbe94653a4a8d6bcf97ad480b386ba5cf8f94fe6b +django-bootstrap3==22.1 \ + --hash=sha256:6ac3af76ec81df53d81f01a19cac1565ca24d5b036a0e636815120a212270d14 \ + --hash=sha256:bab5113b0c865dd644d317d73ae2c3b716215b1bcfacc671124f9dffb823246a # via # -r requirements.in # django-dbmi-client -django-countries==6.1.3 \ - --hash=sha256:64015977a5989bcb0e645007299b19fe8ac117466af375161b26bcfa32ae2808 \ - --hash=sha256:a0f77154ae08cb38a0d65530a399ead5f5837ebf6c74f7576e71bb7acdacca94 +django-countries==7.3.2 \ + --hash=sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82 \ + --hash=sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9 # via -r requirements.in -django-dbmi-client==0.5.2 \ - --hash=sha256:270fff5b6f089858b7bb6f4c3fe64c8d616348146feb5b8c6c1f158f1dab144d \ - --hash=sha256:4dbd944c7fd5d4f13ce073facb761df175057d24ff4e28153a8a97e38c5d93cd +django-dbmi-client==0.5.4 \ + --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ + --hash=sha256:e37597b1f0de4f62d1121a1e27eb830291c6e8ef714e8939a8ac9507486192f5 # via -r requirements.in -django-health-check==3.16.5 \ - --hash=sha256:1edfd49293ccebbce29f9da609c407f307aee240ab799ab4201031341ae78c0f \ - --hash=sha256:8d66781a0ea82b1a8b44878187b38a27370e94f18287312e39be0593e72d8983 +django-health-check==3.17.0 \ + --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ + --hash=sha256:d1b8671e79d1de6e3dd1a9c69566222b0bfcfacca8b90511a4407b2d0d3d2778 # via -r requirements.in django-jquery==3.1.0 \ --hash=sha256:414ac1083708ac71cc771612e7287702c939d06cd357fd53bb1271a4f6255115 \ @@ -183,9 +191,9 @@ django-jsonfield-backport==1.0.5 \ --hash=sha256:40c39b1f1bade47bd2715664de14983f2e0a96175f65abbad2688029c255c685 \ --hash=sha256:727b402bad632e38539b489e48aefa6ffe03cedba76ac1f11b455423b3225a2d # via -r requirements.in -django-picklefield==3.0.1 \ - --hash=sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287 \ - --hash=sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35 +django-picklefield==3.1 \ + --hash=sha256:c786cbeda78d6def2b43bff4840d19787809c8909f7ad683961703060398d356 \ + --hash=sha256:d77c504df7311e8ec14e8b779f10ca6fec74de6c7f8e2c136e1ef60cf955125d # via # -r requirements.in # django-q @@ -196,16 +204,16 @@ django-q==1.3.9 \ django-smtp-ssl==1.0 \ --hash=sha256:282863d5905e03686b6555ac788aa732842695d6f9cf1dcfa66d898abb7565d0 # via -r requirements.in -django-storages==1.12.3 \ - --hash=sha256:204a99f218b747c46edbfeeb1310d357f83f90fa6a6024d8d0a3f422570cee84 \ - --hash=sha256:a475edb2f0f04c4f7e548919a751ecd50117270833956ed5bd585c0575d2a5e7 +django-storages==1.13.1 \ + --hash=sha256:3540b45618b04be2c867c0982e8d2bd8e34f84dae922267fcebe4691fb93daf0 \ + --hash=sha256:b3d98ecc09f1b1627c2b2cf430964322ce4e08617dbf9b4236c16a32875a1e0b # via -r requirements.in django-stronghold==0.4.0 \ --hash=sha256:4127d5f9c11f6582a1c03e7758256b1fe5c872f64f212980e5ad5c67f5eeaa3d # via -r requirements.in -djangorestframework==3.13.1 \ - --hash=sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee \ - --hash=sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa +djangorestframework==3.14.0 \ + --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \ + --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08 # via # -r requirements.in # django-dbmi-client @@ -219,9 +227,9 @@ furl==2.1.3 \ # via # -r requirements.in # django-dbmi-client -idna==3.3 \ - --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ - --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d +idna==3.4 \ + --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ + --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests jmespath==1.0.1 \ --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ @@ -229,8 +237,8 @@ jmespath==1.0.1 \ # via # boto3 # botocore -jwcrypto==1.3.1 \ - --hash=sha256:54b551b115ffb4d12b1f1ee93b8ba2a71bb8556ba3d85d62f707549613da877c +jwcrypto==1.4.2 \ + --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b # via # -r requirements.in # django-dbmi-client @@ -255,9 +263,9 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pyjwt==2.4.0 \ - --hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \ - --hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba +pyjwt==2.5.0 \ + --hash=sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80 \ + --hash=sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b # via # -r requirements.in # django-dbmi-client @@ -272,12 +280,10 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.1 \ - --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ - --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c - # via - # django - # djangorestframework +pytz==2022.2.1 \ + --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ + --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 + # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \ @@ -344,13 +350,17 @@ six==1.16.0 \ # furl # orderedmultidict # python-dateutil -sqlparse==0.4.2 \ - --hash=sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae \ - --hash=sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d +sqlparse==0.4.3 \ + --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \ + --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268 # via django -urllib3==1.26.11 \ - --hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \ - --hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a +typing-extensions==4.3.0 \ + --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ + --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 + # via django-countries +urllib3==1.26.12 \ + --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ + --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 # via # botocore # requests From 3a67912680749a57d63d6fb5462cf7f18202ec39 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 5 Oct 2022 12:41:27 -0600 Subject: [PATCH 550/613] DBMISVC-120 - Updated to support latest version of dbmi-client with Cognito support; cleaned up settings to remove redundant settings; updated base Docker image --- Dockerfile | 4 +- app/hypatio/auth0authenticate.py | 261 +------------------ app/hypatio/dbmiauthz_services.py | 10 +- app/hypatio/sciauthz_services.py | 28 +- app/hypatio/scireg_services.py | 19 +- app/hypatio/settings.py | 31 +-- app/manage/api.py | 54 ++-- app/manage/views.py | 10 +- app/profile/views.py | 27 +- app/projects/api.py | 8 +- app/projects/templatetags/projects_extras.py | 10 +- app/projects/views.py | 7 +- requirements.in | 2 - requirements.txt | 119 ++------- 14 files changed, 111 insertions(+), 479 deletions(-) diff --git a/Dockerfile b/Dockerfile index c596f2c3..ec9dfa65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 AS builder +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 AS builder # Install requirements RUN apt-get update \ @@ -19,7 +19,7 @@ RUN pip install -U wheel \ && pip wheel -r /requirements.txt \ --wheel-dir=/root/wheels -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels diff --git a/app/hypatio/auth0authenticate.py b/app/hypatio/auth0authenticate.py index 3fcd9dbd..8e65a49c 100644 --- a/app/hypatio/auth0authenticate.py +++ b/app/hypatio/auth0authenticate.py @@ -1,10 +1,8 @@ -import jwt from furl import furl import json import base64 import logging import requests -import jwcrypto.jwk as jwk from functools import wraps from django.contrib.auth.models import User @@ -14,6 +12,9 @@ from django.shortcuts import redirect from django.contrib.auth import logout from django.core.exceptions import PermissionDenied +from dbmi_client.settings import dbmi_settings +from dbmi_client.settings import dbmi_settings +from dbmi_client.authn import validate_request, login_redirect_url logger = logging.getLogger(__name__) @@ -46,7 +47,7 @@ def wrap(request, *args, **kwargs): # Confirm user is a manager of the given project permissions_url = sciauthz_permission_url(item, email) - response = requests.get(permissions_url, headers=sciauthz_headers(request), verify=verify_requests()) + response = requests.get(permissions_url, headers=sciauthz_headers(request)) content = response.content response.raise_for_status() @@ -191,22 +192,6 @@ def sciauthz_permission_url(item, email): return url.url -def verify_requests(): - ''' - Checks settings to see if requests should be verified, defaults to True - :return: Whether to verify requests or not - :rtype: bool - ''' - # Check for setting on verifying requests - if hasattr(settings, 'VERIFY_REQUESTS'): - return settings.VERIFY_REQUESTS - - # Log it - logger.debug('VERIFY_REQUESTS setting is missing, defaulting to "True"') - - return True - - def sciauthz_headers(request): ''' Returns the headers needed to authenticate requests against SciAuthZ @@ -222,239 +207,6 @@ def sciauthz_headers(request): return {"Authorization": "JWT " + jwt_string, 'Content-Type': 'application/json'} -def validate_jwt(request): - """ - Determines if the JWT is valid based on expiration and signature evaluation. - :param request: - :return: None if JWT is invalid or missing. - """ - # Extract JWT token into a string. - jwt_string = request.COOKIES.get("DBMI_JWT", None) - - # Check that we actually have a token. - if jwt_string is not None: - - # Attempt to validate the JWT (Checks both expiry and signature) - try: - payload = jwt.decode(jwt_string, - base64.b64decode(settings.AUTH0_SECRET, '-_'), - algorithms=['HS256'], - leeway=120, - audience=settings.AUTH0_CLIENT_ID) - - return payload - - except jwt.ExpiredSignatureError: - logger.debug("JWT Expired.") - - except jwt.InvalidTokenError: - logger.debug("Invalid JWT Token.") - - except Exception as e: - logger.exception('Unexpected validation error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string}) - - return None - - -def validate_request(request): - ''' - Pulls the current cookie and verifies the JWT and - then returns the JWT payload. Returns None - if the JWT is invalid or missing. - :param request: The Django request object - :return: dict - ''' - # Extract JWT token into a string. - jwt_string = request.COOKIES.get("DBMI_JWT", None) - - # Check that we actually have a token. - if jwt_string is not None: - return validate_rs256_jwt(jwt_string) - else: - return None - - -def get_public_keys_from_auth0(refresh=False): - ''' - Retrieves the public key from Auth0 to verify JWTs. Will - cache the JSON response from Auth0 in Django settings - until instructed to refresh the JWKS. - :param refresh: Purges cached JWK and fetches from remote - :return: dict - ''' - - # If refresh, delete cached key - if refresh: - delattr(settings, 'AUTH0_JWKS') - - jwks = None - content = None - try: - # Look in settings - if hasattr(settings, 'AUTH0_JWKS'): - logger.debug('Using cached JWKS') - - # Parse the cached dict and return it - return json.loads(settings.AUTH0_JWKS) - - else: - - logger.debug('Fetching remote JWKS') - - # Make the request - response = requests.get("https://" + settings.AUTH0_DOMAIN + "/.well-known/jwks.json") - content = response.content - response.raise_for_status() - - # Parse it - jwks = response.json() - - # Cache it - setattr(settings, 'AUTH0_JWKS', json.dumps(jwks)) - - return jwks - - except KeyError as e: - logging.exception('Parsing public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'jwks': jwks}) - - except json.JSONDecodeError as e: - logging.exception('Parsing public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'jwks': jwks}) - - except requests.HTTPError as e: - logging.exception('Gettting public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'content': content}) - - except Exception as e: - logging.exception('Unexpected public key error: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'content': content}) - - return None - - -def retrieve_public_key(jwt_string): - ''' - Gets the public key used to sign the JWT from the public JWK - hosted on Auth0. Auth0 typically only returns one public key - in the JWK set but to handle situations in which signing keys - are being rotated, this method is build to search through - multiple JWK that could be in the set. - - As JWKS are being cached, if a JWK cannot be found, cached - JWKS is purged and a new JWKS is fetched from Auth0. The - fresh JWKS is then searched again for the needed key. - - Returns the key ID if found, otherwise returns None - :param jwt_string: The JWT token as a string - :return: str - ''' - - try: - # Get the JWK - jwks = get_public_keys_from_auth0(refresh=False) - - # Decode the JWTs header component - unverified_header = jwt.get_unverified_header(str(jwt_string)) - - # Check the JWK for the key the JWT was signed with - rsa_key = get_rsa_from_jwks(jwks, unverified_header['kid']) - if not rsa_key: - logger.debug('No matching key found in JWKS, refreshing') - logger.debug('Unverified JWT key id: {}'.format(unverified_header['kid'])) - logger.debug('Cached JWK keys: {}'.format([jwk['kid'] for jwk in jwks['keys']])) - - # No match found, refresh the jwks - jwks = get_public_keys_from_auth0(refresh=True) - logger.debug('Refreshed JWK keys: {}'.format([jwk['kid'] for jwk in jwks['keys']])) - - # Try it again - rsa_key = get_rsa_from_jwks(jwks, unverified_header['kid']) - if not rsa_key: - logger.warning('No matching key found despite refresh, failing: {}'.format(unverified_header.get('kid'))) - - return rsa_key - - except jwt.exceptions.DecodeError as e: - logger.debug('Invalid JWT used: {}'.format(e)) - - except KeyError as e: - logger.exception('Comparing public key failed: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'jwks': jwks}) - - except Exception as e: - logger.exception('Unexpected public key error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'jwks': jwks}) - - return None - - -def get_rsa_from_jwks(jwks, jwt_kid): - ''' - Searches the JWKS for the signing key used - for the JWT. Returns a dict of the JWK - properties if found, None otherwise. - :param jwks: The set of JWKs from Auth0 - :param jwt_kid: The key ID of the signing key - :return: dict - ''' - # Build the dict containing rsa values - for key in jwks["keys"]: - if key["kid"] == jwt_kid: - rsa_key = { - "kty": key["kty"], - "kid": key["kid"], - "use": key["use"], - "n": key["n"], - "e": key["e"] - } - - return rsa_key - - # No matching key found, must refresh JWT keys - return None - - -def validate_rs256_jwt(jwt_string): - ''' - Verifies the given RS256 JWT. Returns the payload - if verified, otherwise returns None. - :param jwt_string: JWT as a string - :return: dict - ''' - - rsa_pub_key = retrieve_public_key(jwt_string) - payload = None - - if rsa_pub_key: - jwk_key = jwk.JWK(**rsa_pub_key) - - # Iterate each possible client ID - for client_id in settings.AUTH0_CLIENT_ID_LIST: - - # Attempt to validate the JWT (Checks both expiry and signature) - try: - payload = jwt.decode(jwt_string, - jwk_key.export_to_pem(private_key=False), - algorithms=['RS256'], - leeway=120, - audience=client_id) - return payload - - except jwt.ExpiredSignatureError: - logger.debug("JWT Expired.") - - except jwt.InvalidTokenError: - logger.debug("Invalid JWT Token.") - - except Exception as e: - logger.exception('Unexpected validation error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'auth0_client_id': client_id}) - - return None - - def jwt_login(request, jwt_payload): """ The user has a valid JWT but needs to log into this app. Do so here and return the status. @@ -486,8 +238,7 @@ def logout_redirect(request): logout(request) # Build the URL - login_url = furl(settings.AUTHENTICATION_LOGIN_URL) - login_url.query.params.add('next', request.build_absolute_uri()) + login_url = furl(login_redirect_url(request, next_url=request.build_absolute_uri())) # Check for branding if hasattr(settings, 'SCIAUTH_BRANDING'): @@ -499,7 +250,7 @@ def logout_redirect(request): # Set the URL and purge cookies response = redirect(login_url.url) - response.delete_cookie('DBMI_JWT', domain=settings.COOKIE_DOMAIN) + response.delete_cookie('DBMI_JWT', domain=dbmi_settings.JWT_COOKIE_DOMAIN) logger.debug('Redirecting to: {}'.format(login_url.url)) return response diff --git a/app/hypatio/dbmiauthz_services.py b/app/hypatio/dbmiauthz_services.py index 84b54376..8cf885b1 100644 --- a/app/hypatio/dbmiauthz_services.py +++ b/app/hypatio/dbmiauthz_services.py @@ -2,8 +2,8 @@ from furl import furl import logging -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from dbmi_client.settings import dbmi_settings from projects.models import DataProject from projects.models import Participant @@ -13,10 +13,10 @@ class DBMIAuthz: - user_permissions_url = settings.AUTHZ_BASE + "/user_permission/" - create_profile_permission_url = settings.AUTHZ_BASE + "/user_permission/create_registration_permission_record/" - create_view_permission_url = settings.AUTHZ_BASE + "/user_permission/create_item_view_permission_record/" - remove_view_permission_url = settings.AUTHZ_BASE + "/user_permission/remove_item_view_permission_record/" + user_permissions_url = dbmi_settings.AUTHZ_URL + "/user_permission/" + create_profile_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/create_registration_permission_record/" + create_view_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/create_item_view_permission_record/" + remove_view_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/remove_item_view_permission_record/" @classmethod def _permissions_query(cls, request, email=None, item=None, search=None): diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index bde7c2ed..780d2a32 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -5,7 +5,7 @@ import furl import logging -from django.conf import settings +from dbmi_client.settings import dbmi_settings from projects.models import DataProject @@ -16,17 +16,12 @@ class SciAuthZ: JWT_HEADERS = None CURRENT_USER_EMAIL = None - def __init__(self, authz_base, jwt, user_email): + def __init__(self, jwt, user_email): - user_permissions_url = authz_base + "/user_permission/" - create_profile_permission = authz_base + "/user_permission/create_registration_permission_record/" - create_view_permission = authz_base + "/user_permission/create_item_view_permission_record/" - remove_view_permission = authz_base + "/user_permission/remove_item_view_permission_record/" - - self.USER_PERMISSIONS_URL = user_permissions_url - self.CREATE_PROFILE_PERMISSION = create_profile_permission - self.CREATE_ITEM_PERMISSION = create_view_permission - self.REMOVE_ITEM_PERMISSION = remove_view_permission + self.USER_PERMISSIONS_URL = dbmi_settings.AUTHZ_URL + "/user_permission/" + self.CREATE_PROFILE_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/create_registration_permission_record/" + self.CREATE_ITEM_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/create_item_view_permission_record/" + self.REMOVE_ITEM_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/remove_item_view_permission_record/" jwt_headers = {"Authorization": "JWT " + jwt, 'Content-Type': 'application/json'} @@ -45,7 +40,7 @@ def user_has_manage_permission(self, item): permissions_url.query.params.add('search', 'Hypatio,MANAGE') try: - user_permissions = requests.get(permissions_url.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() + user_permissions = requests.get(permissions_url.url, headers=self.JWT_HEADERS).json() except JSONDecodeError: user_permissions = None @@ -74,7 +69,6 @@ def current_user_permissions(self): user_permissions_request = requests.get( authz_url.url, headers=self.JWT_HEADERS, - verify=settings.VERIFY_REQUESTS ).json() # If there are any permissions returned, add them to the list. @@ -108,7 +102,6 @@ def create_profile_permission(self, grantee_email, project): self.CREATE_PROFILE_PERMISSION, headers=modified_headers, data=data, - verify=settings.VERIFY_REQUESTS ) return profile_permission @@ -124,7 +117,7 @@ def create_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) + view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context) return view_permission def remove_view_permission(self, project, grantee_email): @@ -138,7 +131,7 @@ def remove_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) + view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context) return view_permission def user_has_single_permission(self, permission, value, email=None): @@ -151,7 +144,7 @@ def user_has_single_permission(self, permission, value, email=None): f.args["email"] = email try: - user_permissions = requests.get(f.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() + user_permissions = requests.get(f.url, headers=self.JWT_HEADERS).json() except JSONDecodeError: logger.debug("[SCIAUTHZ][user_has_single_permission] - No Valid permissions returned.") return False @@ -217,7 +210,6 @@ def get_all_view_permissions_for_project(self, project): user_permissions_request = requests.get( authz_url, headers=self.JWT_HEADERS, - verify=settings.VERIFY_REQUESTS ).json() # If there are any permissions returned, add them to the list. diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index 587231e7..93e1c5e2 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -4,21 +4,20 @@ from furl import furl from json import JSONDecodeError -from django.conf import settings - +from dbmi_client.settings import dbmi_settings import logging logger = logging.getLogger(__name__) -VERIFY_SSL = True - +# Set the base API URL for registration related queries +DBMI_REG_API_URL = furl(dbmi_settings.REG_URL) / "api" / "register" def build_headers_with_jwt(user_jwt): return {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} def send_confirmation_email(user_jwt, current_uri): - send_confirm_email_url = settings.SCIREG_REGISTRATION_URL + 'send_confirmation_email/' + send_confirm_email_url = (DBMI_REG_API_URL / 'send_confirmation_email/').url logger.debug("[HYPATIO][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) @@ -36,7 +35,7 @@ def get_user_email_confirmation_status(user_jwt): Returns True or False. """ - response = requests.get(settings.SCIREG_REGISTRATION_URL, headers=build_headers_with_jwt(user_jwt)) + response = requests.get(DBMI_REG_API_URL.url, headers=build_headers_with_jwt(user_jwt)) try: email_status = response.json()['results'][0]['email_confirmed'] @@ -50,7 +49,7 @@ def get_user_email_confirmation_status(user_jwt): def get_current_user_profile(user_jwt): - f = furl(settings.SCIREG_REGISTRATION_URL) + f = furl(DBMI_REG_API_URL.url) try: profile = requests.get(f.url, headers=build_headers_with_jwt(user_jwt)).json() @@ -63,7 +62,7 @@ def get_current_user_profile(user_jwt): def get_user_profile(user_jwt, email_of_profile, project_key): - f = furl(settings.SCIREG_REGISTRATION_URL) + f = furl(DBMI_REG_API_URL.url) f.args["email"] = email_of_profile f.args["project"] = 'Hypatio.' + project_key @@ -82,7 +81,7 @@ def get_distinct_countries_participating(user_jwt, participants, project_key): containing the unique countries of these participants and a count for each. """ - url = settings.SCIREG_REGISTRATION_URL + 'get_countries/' + url = (DBMI_REG_API_URL / 'get_countries/').url # From a QuerySet of participants, get a list of their emails emails = [participant.user.email for participant in participants] @@ -108,7 +107,7 @@ def get_names(user_jwt, participants, project_key): containing the first and last names of each participant. """ - url = settings.SCIREG_REGISTRATION_URL + 'get_names/' + url = (DBMI_REG_API_URL.url / 'get_names/').url # From a QuerySet of participants, get a list of their emails emails = list(participants.values_list('user__email', flat=True)) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index e955fa1a..fdd870c4 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -128,25 +128,8 @@ SITE_URL = environment.get_str("SITE_URL", required=True) -AUTH0_DOMAIN = environment.get_str("AUTH0_DOMAIN", required=True) -AUTH0_CLIENT_ID_LIST = environment.get_list("AUTH0_CLIENT_ID_LIST", required=True) -AUTH0_SECRET = environment.get_str("AUTH0_SECRET", required=True) -AUTH0_SUCCESS_URL = environment.get_str("AUTH0_SUCCESS_URL", required=True) -AUTH0_LOGOUT_URL = environment.get_str("AUTH0_LOGOUT_URL", required=True) - AUTHENTICATION_BACKENDS = ['hypatio.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend'] -AUTHENTICATION_LOGIN_URL = environment.get_str("ACCOUNT_SERVER_URL", required=True) -ACCOUNT_SERVER_URL = environment.get_str("ACCOUNT_SERVER_URL", required=True) -SCIREG_SERVER_URL = environment.get_str("SCIREG_SERVER_URL", required=True) -AUTHZ_BASE = environment.get_str("AUTHZ_BASE", required=True) - -USER_PERMISSIONS_URL = AUTHZ_BASE + "/user_permission/" - -SCIREG_REGISTRATION_URL = SCIREG_SERVER_URL + "/api/register/" - -COOKIE_DOMAIN = environment.get_str("COOKIE_DOMAIN", required=True) - SSL_SETTING = "https" VERIFY_REQUESTS = True @@ -156,8 +139,6 @@ RECAPTCHA_KEY = environment.get_str('RECAPTCHA_KEY', required=True) RECAPTCHA_CLIENT_ID = environment.get_str('RECAPTCHA_CLIENT_ID', required=True) -EMAIL_CONFIRM_SUCCESS_URL = environment.get_str('EMAIL_CONFIRM_SUCCESS_URL', required=True) - ########## # S3 Configurations S3_BUCKET = environment.get_str('S3_BUCKET', required=True) @@ -234,12 +215,11 @@ 'AUTHZ_ADMIN_GROUP': 'hypatio-admins', 'AUTHZ_ADMIN_PERMISSION': 'ADMIN', 'JWT_COOKIE_DOMAIN': environment.get_str('COOKIE_DOMAIN', required=True), - - # Auth0 - 'AUTH0_TENANT': AUTH0_DOMAIN.lower().replace(".auth0.com", ""), - 'AUTH0_CLIENT_ID': next(iter(AUTH0_CLIENT_ID_LIST)), 'AUTHN_TITLE': 'DBMI Portal', + # Set auth configurations + 'AUTH_CLIENTS': environment.get_dict('AUTH_CLIENTS', required=True), + # Fileservice 'FILESERVICE_URL': environment.get_str('FILESERVICE_API_URL', required=True), 'FILESERVICE_GROUP': environment.get_str('FILESERVICE_GROUP', required=True), @@ -364,8 +344,3 @@ 'release': '1', 'site': 'HYPATIO' } - -try: - from .local_settings import * -except ImportError: - pass diff --git a/app/manage/api.py b/app/manage/api.py index ad18a491..d55539bb 100644 --- a/app/manage/api.py +++ b/app/manage/api.py @@ -57,7 +57,7 @@ def set_dataproject_registration_status(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -101,7 +101,7 @@ def set_dataproject_visible_status(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -145,7 +145,7 @@ def set_dataproject_details(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -200,7 +200,7 @@ def get_static_agreement_form_html(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -250,7 +250,7 @@ def get_hosted_file_edit_form(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -293,7 +293,7 @@ def get_hosted_file_logs(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -350,7 +350,7 @@ def process_hosted_file_edit_form_submission(request): project_id = request.POST.get("project") project = get_object_or_404(DataProject, id=project_id) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -393,7 +393,7 @@ def download_signed_form(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -448,7 +448,7 @@ def get_signed_form_status(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -482,7 +482,7 @@ def change_signed_form_status(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -528,7 +528,7 @@ def change_signed_form_status(request): team.save() for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(signed_form.project.project_key, member.user.email) # Remove their VIEW permission @@ -573,7 +573,7 @@ def save_team_comment(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -609,7 +609,7 @@ def set_team_status(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) logger.debug( @@ -652,7 +652,7 @@ def set_team_status(request): # If setting to Active, grant each team member access permissions. if status == "active": for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_view_permission(project_key, member.user.email) # Add permission to Participant @@ -662,7 +662,7 @@ def set_team_status(request): # If setting to Deactivated, revoke each team member's permissions. elif status == "deactivated": for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant @@ -707,7 +707,7 @@ def delete_team(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -725,7 +725,7 @@ def delete_team(request): # First revoke all VIEW permissions for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant @@ -769,7 +769,7 @@ def download_team_submissions(request, project_key, team_leader_email): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -834,7 +834,7 @@ def download_submission(request, fileservice_uuid): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -867,7 +867,7 @@ def export_submissions(request, project_key): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -893,7 +893,7 @@ def download_submissions_export(request, project_key, fileservice_uuid): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -934,7 +934,7 @@ def host_submission(request, fileservice_uuid): submission = get_object_or_404(ChallengeTaskSubmission, uuid=fileservice_uuid) project = submission.challenge_task.data_project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -986,7 +986,7 @@ def host_submission(request, fileservice_uuid): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -1041,7 +1041,7 @@ def download_email_list(request): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -1113,7 +1113,7 @@ def grant_view_permission(request, project_key, user_email): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( @@ -1182,7 +1182,7 @@ def remove_view_permission(request, project_key, user_email): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( @@ -1232,7 +1232,7 @@ def sync_view_permissions(request, project_key): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( diff --git a/app/manage/views.py b/app/manage/views.py index f196b56c..c27d44bc 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -66,7 +66,7 @@ def get_context_data(self, **kwargs): context = super(DataProjectListManageView, self).get_context_data(**kwargs) user_jwt = self.request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, self.request.user.email) + sciauthz = SciAuthZ(user_jwt, self.request.user.email) projects_managed = sciauthz.get_projects_managed_by_user() context['projects'] = projects_managed @@ -99,7 +99,7 @@ def dispatch(self, request, *args, **kwargs): user_jwt = request.COOKIES.get("DBMI_JWT", None) - self.sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + self.sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = self.sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -539,7 +539,7 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -671,7 +671,7 @@ def get(self, request, project_key, user_email, *args, **kwargs): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -704,7 +704,7 @@ def post(self, request, project_key, user_email, *args, **kwargs): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: diff --git a/app/profile/views.py b/app/profile/views.py index 7935a322..14e50186 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -2,20 +2,22 @@ import logging import requests -from hypatio.auth0authenticate import user_auth_and_jwt -from hypatio.auth0authenticate import validate_request as validate_jwt -from hypatio.auth0authenticate import logout_redirect - from django.conf import settings from django.contrib import messages from django.contrib.auth import logout from django.http import HttpResponse from django.shortcuts import redirect from django.shortcuts import render +from django.urls import reverse +from furl import furl +from dbmi_client.settings import dbmi_settings +from dbmi_client.authn import logout_redirect_url from hypatio import scireg_services - -from .forms import RegistrationForm +from hypatio.auth0authenticate import user_auth_and_jwt +from hypatio.auth0authenticate import validate_request as validate_jwt +from hypatio.auth0authenticate import logout_redirect +from profile.forms import RegistrationForm # Get an instance of a logger logger = logging.getLogger(__name__) @@ -24,8 +26,7 @@ @user_auth_and_jwt def signout(request): logout(request) - response = redirect(settings.AUTH0_LOGOUT_URL) - response.delete_cookie('DBMI_JWT', domain=settings.COOKIE_DOMAIN) + response = redirect(logout_redirect_url(request, request.build_absolute_uri(reverse("index")))) return response @@ -51,12 +52,13 @@ def update_profile(request): logger.debug('[HYPATIO][DEBUG] Profile form fields submitted: ' + json.dumps(registration_form.cleaned_data)) # Create a new registration with a POST + url = furl(dbmi_settings.REG_URL) / "api" / "register" if registration_form.cleaned_data['id'] == "": - requests.post(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) + requests.post((url / "/").url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data)) # Update an existing registration with a PUT to the specific ID else: - registration_url = settings.SCIREG_REGISTRATION_URL + registration_form.cleaned_data['id'] + '/' - requests.put(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) + url.path.segments.extend([registration_form.cleaned_data['id'], ""]) + requests.put(url.url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data)) return HttpResponse(200) else: @@ -74,7 +76,8 @@ def profile(request, template_name='profile/profile.html'): jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} # Query SciReg to get the user's information - registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() + url = furl(dbmi_settings.REG_URL) / "api" / "register" / "/" + registration_info = requests.get(url.url, headers=jwt_headers).json() logger.debug('[HYPATIO][DEBUG] Registration info ' + json.dumps(registration_info)) diff --git a/app/projects/api.py b/app/projects/api.py index bdb67dc7..024342c6 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -214,7 +214,7 @@ def leave_team(request): # TODO user does not have permissions to remove their view permission (whether or not it exists) # Remove VIEW permissions on the DataProject - # sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + # sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) # sciauthz.remove_view_permission(project_key, request.user.email) # TODO remove team leader's scireg permissions @@ -296,7 +296,7 @@ def join_team(request): extra=context) # Create record to allow leader access to profile. - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_profile_permission(team_leader, project_key) return redirect('/projects/' + request.POST.get('project_key') + '/') @@ -417,7 +417,7 @@ def upload_challengetasksubmission_file(request): # Check that user has permissions to be submitting files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) if not sciauthz.user_has_single_permission(project_key, "VIEW", request.user.email): logger.warning(f"[{project_key}][{request.user.email}] No Access") @@ -540,7 +540,7 @@ def delete_challengetasksubmission(request): # Check that user has permissions to be viewing files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) submission_uuid = request.POST.get('submission_uuid') submission = ChallengeTaskSubmission.objects.get(uuid=submission_uuid) diff --git a/app/projects/templatetags/projects_extras.py b/app/projects/templatetags/projects_extras.py index cad682d6..c7403e2d 100644 --- a/app/projects/templatetags/projects_extras.py +++ b/app/projects/templatetags/projects_extras.py @@ -1,13 +1,12 @@ -import os import datetime -import furl +from furl import furl import logging from django import template from django.conf import settings -from django.utils.safestring import mark_safe from django.utils.timezone import utc from django.template.loader import render_to_string +from dbmi_client.authn import login_redirect_url from hypatio.dbmiauthz_services import DBMIAuthz @@ -24,10 +23,7 @@ def get_html_form_file_contents(form_file_path): def get_login_url(current_uri): # Build the login URL - login_url = furl.furl(settings.ACCOUNT_SERVER_URL) - - # Add the next URL - login_url.args.add('next', current_uri) + login_url = furl(login_redirect_url(None, next_url=current_uri)) # Add project, if any project = getattr(settings, 'PROJECT', None) diff --git a/app/projects/views.py b/app/projects/views.py index b263a9b1..ed536bef 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -10,16 +10,14 @@ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import TemplateView + from hypatio.sciauthz_services import SciAuthZ from hypatio.dbmiauthz_services import DBMIAuthz from hypatio.scireg_services import get_current_user_profile from hypatio.scireg_services import get_user_email_confirmation_status - from profile.forms import RegistrationForm - from hypatio.auth0authenticate import public_user_auth_and_jwt from hypatio.auth0authenticate import user_auth_and_jwt - from projects.models import AGREEMENT_FORM_TYPE_EXTERNAL_LINK, TEAM_ACTIVE, TEAM_READY from projects.models import AGREEMENT_FORM_TYPE_STATIC from projects.models import AGREEMENT_FORM_TYPE_MODEL @@ -29,7 +27,6 @@ from projects.models import HostedFile from projects.models import Participant from projects.models import SignedAgreementForm - from projects.panels import SIGNUP_STEP_COMPLETED_STATUS from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.panels import SIGNUP_STEP_FUTURE_STATUS @@ -50,7 +47,7 @@ def signed_agreement_form(request): signed_agreement_form_id = request.GET['signed_form_id'] user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) project = get_object_or_404(DataProject, project_key=project_key) diff --git a/requirements.in b/requirements.in index 3eb75790..f81925dd 100644 --- a/requirements.in +++ b/requirements.in @@ -16,9 +16,7 @@ djangorestframework<4.0 django-smtp-ssl<2.0 django-q<2.0 furl<3.0 -jwcrypto<2.0 mysqlclient<3.0 -pyjwt<3.0 python-dateutil<3.0 python-magic<2.0 raven<7.0 diff --git a/requirements.txt b/requirements.txt index db18352e..16af615c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,21 +12,21 @@ asgiref==3.5.2 \ --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 # via django -awscli==1.25.83 \ - --hash=sha256:1269504799e81d05398248ab34c0c3a39a8b4a96e551bf60fcc9932962eed463 \ - --hash=sha256:73acd376a4ebfa97fe6d8bf2a012de37410685f666f5cebce9fa60980d145737 +awscli==1.25.87 \ + --hash=sha256:479556c396e7e77a67463e543f3e32ae41ac268ae7f6a788165f3886c58d8d70 \ + --hash=sha256:e2ba3a1dc593fdcf12bf2ff3f150ec731229d643f6e9b4cedb6b01401c2bb5eb # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.82 \ - --hash=sha256:2a9b283cd4208cfcea873baac29e5ceb011afbd5a5c5b7a35cb305f377c39f60 \ - --hash=sha256:90d590eda672775ef5ad0cdd4f5c2a51a964e3de631f3deccbc96e29cf0cd5a9 +boto3==1.24.86 \ + --hash=sha256:59e9c39c867b5fd0575a50d8fafdf72c823c5510254a8615dff24f1584408121 \ + --hash=sha256:c1d4e5d73d7720402c6538965efd6df5807492db8a71f30221dc11355d06db05 # via -r requirements.in -botocore==1.27.82 \ - --hash=sha256:034cdcfb74bcca9124fcaafed857ad78ec572ff95ed802b0608fa7505aaf4a1f \ - --hash=sha256:b122c7048a79fef53f3d45f7ca2999c2559e15fc4531653824ebd3154d77ede5 +botocore==1.27.86 \ + --hash=sha256:830359c8bf22f2a386431825060ff9666519c032f2ccd4a69f1a702f7195f6c0 \ + --hash=sha256:dcfe1825b84d17cf11653f8bb4bc77c2f172dcec1dc68eee932d68e5e6ed89d5 # via # awscli # boto3 @@ -136,16 +136,10 @@ cryptography==38.0.1 \ --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 - # via - # django-dbmi-client - # jwcrypto -deprecated==1.2.13 \ - --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ - --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d - # via jwcrypto -django==4.1.1 \ - --hash=sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e \ - --hash=sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e + # via django-dbmi-client +django==4.1.2 \ + --hash=sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793 \ + --hash=sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -175,9 +169,9 @@ django-countries==7.3.2 \ --hash=sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82 \ --hash=sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9 # via -r requirements.in -django-dbmi-client==0.5.4 \ - --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ - --hash=sha256:e37597b1f0de4f62d1121a1e27eb830291c6e8ef714e8939a8ac9507486192f5 +django-dbmi-client==1.0.1 \ + --hash=sha256:0bbe719dae983c5c80d6d6e8b7787dd78c212f4bc348b552ab7721726711d692 \ + --hash=sha256:39bb62beb6f0c3e08002cb32e43caed407a0e1642db04ddd2ca3e869ce46c629 # via -r requirements.in django-health-check==3.17.0 \ --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ @@ -237,11 +231,6 @@ jmespath==1.0.1 \ # via # boto3 # botocore -jwcrypto==1.4.2 \ - --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b - # via - # -r requirements.in - # django-dbmi-client mysqlclient==2.1.1 \ --hash=sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c \ --hash=sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782 \ @@ -266,9 +255,7 @@ pycparser==2.21 \ pyjwt==2.5.0 \ --hash=sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80 \ --hash=sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b - # via - # -r requirements.in - # django-dbmi-client + # via django-dbmi-client python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 @@ -280,9 +267,9 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.2.1 \ - --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ - --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 +pytz==2022.4 \ + --hash=sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91 \ + --hash=sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174 # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ @@ -368,69 +355,3 @@ wcwidth==0.2.5 \ --hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \ --hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83 # via blessed -wrapt==1.14.1 \ - --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ - --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ - --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ - --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ - --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ - --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ - --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ - --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ - --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ - --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ - --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ - --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ - --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ - --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ - --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ - --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ - --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ - --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ - --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ - --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ - --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ - --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ - --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ - --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ - --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ - --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ - --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ - --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ - --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ - --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ - --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ - --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ - --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ - --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ - --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ - --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ - --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ - --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ - --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ - --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ - --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ - --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ - --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ - --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ - --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ - --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ - --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ - --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ - --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ - --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ - --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ - --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ - --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ - --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ - --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ - --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ - --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ - --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ - --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ - --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ - --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ - --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ - --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ - --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af - # via deprecated From 46172a8feb5fde150a752266cf1a931fcb516b48 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 5 Oct 2022 12:42:41 -0600 Subject: [PATCH 551/613] DBMISVC-122 - Fixed dbmi-client version pinning --- .pre-commit-config.yaml | 2 +- requirements.in | 2 +- requirements.txt | 30 +++++++++++++++--------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 621451d5..8795174b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: check-merge-conflict - id: detect-aws-credentials - repo: https://github.com/jazzband/pip-tools - rev: 6.6.0 + rev: 6.8.0 hooks: - id: pip-compile name: pip-compile dev-requirements.in diff --git a/requirements.in b/requirements.in index 3eb75790..87f62afa 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ django-autocomplete-light<4.0 django-bootstrap3<23.0 django-bootstrap-datepicker-plus<4.0 django-countries<8.0 -django-dbmi-client<2.0 +django-dbmi-client<1.0 django-health-check<4.0 django-jquery<4.0 django-jsonfield-backport<2.0 diff --git a/requirements.txt b/requirements.txt index db18352e..71589aef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,21 +12,21 @@ asgiref==3.5.2 \ --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 # via django -awscli==1.25.83 \ - --hash=sha256:1269504799e81d05398248ab34c0c3a39a8b4a96e551bf60fcc9932962eed463 \ - --hash=sha256:73acd376a4ebfa97fe6d8bf2a012de37410685f666f5cebce9fa60980d145737 +awscli==1.25.87 \ + --hash=sha256:479556c396e7e77a67463e543f3e32ae41ac268ae7f6a788165f3886c58d8d70 \ + --hash=sha256:e2ba3a1dc593fdcf12bf2ff3f150ec731229d643f6e9b4cedb6b01401c2bb5eb # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.82 \ - --hash=sha256:2a9b283cd4208cfcea873baac29e5ceb011afbd5a5c5b7a35cb305f377c39f60 \ - --hash=sha256:90d590eda672775ef5ad0cdd4f5c2a51a964e3de631f3deccbc96e29cf0cd5a9 +boto3==1.24.86 \ + --hash=sha256:59e9c39c867b5fd0575a50d8fafdf72c823c5510254a8615dff24f1584408121 \ + --hash=sha256:c1d4e5d73d7720402c6538965efd6df5807492db8a71f30221dc11355d06db05 # via -r requirements.in -botocore==1.27.82 \ - --hash=sha256:034cdcfb74bcca9124fcaafed857ad78ec572ff95ed802b0608fa7505aaf4a1f \ - --hash=sha256:b122c7048a79fef53f3d45f7ca2999c2559e15fc4531653824ebd3154d77ede5 +botocore==1.27.86 \ + --hash=sha256:830359c8bf22f2a386431825060ff9666519c032f2ccd4a69f1a702f7195f6c0 \ + --hash=sha256:dcfe1825b84d17cf11653f8bb4bc77c2f172dcec1dc68eee932d68e5e6ed89d5 # via # awscli # boto3 @@ -143,9 +143,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==4.1.1 \ - --hash=sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e \ - --hash=sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e +django==4.1.2 \ + --hash=sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793 \ + --hash=sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -280,9 +280,9 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.2.1 \ - --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ - --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 +pytz==2022.4 \ + --hash=sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91 \ + --hash=sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174 # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ From 8186602cdbfab9ea7e6219396fa27beb69c8ab0d Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 10 Oct 2022 12:09:41 +0000 Subject: [PATCH 552/613] fix(requirements): Updated Python requirements --- requirements.txt | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/requirements.txt b/requirements.txt index 71589aef..2790f17c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,21 +12,21 @@ asgiref==3.5.2 \ --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 # via django -awscli==1.25.87 \ - --hash=sha256:479556c396e7e77a67463e543f3e32ae41ac268ae7f6a788165f3886c58d8d70 \ - --hash=sha256:e2ba3a1dc593fdcf12bf2ff3f150ec731229d643f6e9b4cedb6b01401c2bb5eb +awscli==1.25.90 \ + --hash=sha256:51341ff0e4b1e93e34254f7585c40d6480034df77d6f198ff26418d4c9afd067 \ + --hash=sha256:ec2fa932bee68fe7b6ba83df2343844a7fd9bb74dc26a98386d185860ff8a913 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.86 \ - --hash=sha256:59e9c39c867b5fd0575a50d8fafdf72c823c5510254a8615dff24f1584408121 \ - --hash=sha256:c1d4e5d73d7720402c6538965efd6df5807492db8a71f30221dc11355d06db05 +boto3==1.24.89 \ + --hash=sha256:346f8f0d101a4261dac146a959df18d024feda6431e1d9d84f94efd24d086cae \ + --hash=sha256:d0d8ffcdc10821c4562bc7f935cdd840033bbc342ac0e14b6bdd348b3adf4c04 # via -r requirements.in -botocore==1.27.86 \ - --hash=sha256:830359c8bf22f2a386431825060ff9666519c032f2ccd4a69f1a702f7195f6c0 \ - --hash=sha256:dcfe1825b84d17cf11653f8bb4bc77c2f172dcec1dc68eee932d68e5e6ed89d5 +botocore==1.27.89 \ + --hash=sha256:238f1dfdb8d8d017c2aea082609a3764f3161d32745900f41bcdcf290d95a048 \ + --hash=sha256:621f5413be8f97712b7e36c1b075a8791d1d1b9971a7ee060cdcdf5e2debf6c1 # via # awscli # boto3 @@ -171,9 +171,9 @@ django-bootstrap3==22.1 \ # via # -r requirements.in # django-dbmi-client -django-countries==7.3.2 \ - --hash=sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82 \ - --hash=sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9 +django-countries==7.4.2 \ + --hash=sha256:2eadbe91578fe7b5a0c7782c07487b2ceb4bde9bc5c76eb43458929969f25c62 \ + --hash=sha256:9d553531de8eaf384fec7fdd5867f3cb9b922d384984c3e613b6b29031e5c3c2 # via -r requirements.in django-dbmi-client==0.5.4 \ --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ @@ -354,9 +354,9 @@ sqlparse==0.4.3 \ --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \ --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268 # via django -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via django-countries urllib3==1.26.12 \ --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ From 22ca80097352bc7451236f209f93792cb53025fe Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 5 Oct 2022 12:41:27 -0600 Subject: [PATCH 553/613] DBMISVC-120 - Updated to support latest version of dbmi-client with Cognito support; cleaned up settings to remove redundant settings; updated base Docker image --- Dockerfile | 4 +- app/hypatio/auth0authenticate.py | 261 +------------------ app/hypatio/dbmiauthz_services.py | 10 +- app/hypatio/sciauthz_services.py | 28 +- app/hypatio/scireg_services.py | 19 +- app/hypatio/settings.py | 31 +-- app/manage/api.py | 54 ++-- app/manage/views.py | 10 +- app/profile/views.py | 27 +- app/projects/api.py | 8 +- app/projects/templatetags/projects_extras.py | 10 +- app/projects/views.py | 7 +- requirements.in | 2 - requirements.txt | 89 +------ 14 files changed, 96 insertions(+), 464 deletions(-) diff --git a/Dockerfile b/Dockerfile index c596f2c3..ec9dfa65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 AS builder +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 AS builder # Install requirements RUN apt-get update \ @@ -19,7 +19,7 @@ RUN pip install -U wheel \ && pip wheel -r /requirements.txt \ --wheel-dir=/root/wheels -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels diff --git a/app/hypatio/auth0authenticate.py b/app/hypatio/auth0authenticate.py index 3fcd9dbd..8e65a49c 100644 --- a/app/hypatio/auth0authenticate.py +++ b/app/hypatio/auth0authenticate.py @@ -1,10 +1,8 @@ -import jwt from furl import furl import json import base64 import logging import requests -import jwcrypto.jwk as jwk from functools import wraps from django.contrib.auth.models import User @@ -14,6 +12,9 @@ from django.shortcuts import redirect from django.contrib.auth import logout from django.core.exceptions import PermissionDenied +from dbmi_client.settings import dbmi_settings +from dbmi_client.settings import dbmi_settings +from dbmi_client.authn import validate_request, login_redirect_url logger = logging.getLogger(__name__) @@ -46,7 +47,7 @@ def wrap(request, *args, **kwargs): # Confirm user is a manager of the given project permissions_url = sciauthz_permission_url(item, email) - response = requests.get(permissions_url, headers=sciauthz_headers(request), verify=verify_requests()) + response = requests.get(permissions_url, headers=sciauthz_headers(request)) content = response.content response.raise_for_status() @@ -191,22 +192,6 @@ def sciauthz_permission_url(item, email): return url.url -def verify_requests(): - ''' - Checks settings to see if requests should be verified, defaults to True - :return: Whether to verify requests or not - :rtype: bool - ''' - # Check for setting on verifying requests - if hasattr(settings, 'VERIFY_REQUESTS'): - return settings.VERIFY_REQUESTS - - # Log it - logger.debug('VERIFY_REQUESTS setting is missing, defaulting to "True"') - - return True - - def sciauthz_headers(request): ''' Returns the headers needed to authenticate requests against SciAuthZ @@ -222,239 +207,6 @@ def sciauthz_headers(request): return {"Authorization": "JWT " + jwt_string, 'Content-Type': 'application/json'} -def validate_jwt(request): - """ - Determines if the JWT is valid based on expiration and signature evaluation. - :param request: - :return: None if JWT is invalid or missing. - """ - # Extract JWT token into a string. - jwt_string = request.COOKIES.get("DBMI_JWT", None) - - # Check that we actually have a token. - if jwt_string is not None: - - # Attempt to validate the JWT (Checks both expiry and signature) - try: - payload = jwt.decode(jwt_string, - base64.b64decode(settings.AUTH0_SECRET, '-_'), - algorithms=['HS256'], - leeway=120, - audience=settings.AUTH0_CLIENT_ID) - - return payload - - except jwt.ExpiredSignatureError: - logger.debug("JWT Expired.") - - except jwt.InvalidTokenError: - logger.debug("Invalid JWT Token.") - - except Exception as e: - logger.exception('Unexpected validation error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string}) - - return None - - -def validate_request(request): - ''' - Pulls the current cookie and verifies the JWT and - then returns the JWT payload. Returns None - if the JWT is invalid or missing. - :param request: The Django request object - :return: dict - ''' - # Extract JWT token into a string. - jwt_string = request.COOKIES.get("DBMI_JWT", None) - - # Check that we actually have a token. - if jwt_string is not None: - return validate_rs256_jwt(jwt_string) - else: - return None - - -def get_public_keys_from_auth0(refresh=False): - ''' - Retrieves the public key from Auth0 to verify JWTs. Will - cache the JSON response from Auth0 in Django settings - until instructed to refresh the JWKS. - :param refresh: Purges cached JWK and fetches from remote - :return: dict - ''' - - # If refresh, delete cached key - if refresh: - delattr(settings, 'AUTH0_JWKS') - - jwks = None - content = None - try: - # Look in settings - if hasattr(settings, 'AUTH0_JWKS'): - logger.debug('Using cached JWKS') - - # Parse the cached dict and return it - return json.loads(settings.AUTH0_JWKS) - - else: - - logger.debug('Fetching remote JWKS') - - # Make the request - response = requests.get("https://" + settings.AUTH0_DOMAIN + "/.well-known/jwks.json") - content = response.content - response.raise_for_status() - - # Parse it - jwks = response.json() - - # Cache it - setattr(settings, 'AUTH0_JWKS', json.dumps(jwks)) - - return jwks - - except KeyError as e: - logging.exception('Parsing public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'jwks': jwks}) - - except json.JSONDecodeError as e: - logging.exception('Parsing public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'jwks': jwks}) - - except requests.HTTPError as e: - logging.exception('Gettting public keys failed: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'content': content}) - - except Exception as e: - logging.exception('Unexpected public key error: {}'.format(e), exc_info=True, - extra={'domain': settings.AUTH0_DOMAIN, 'content': content}) - - return None - - -def retrieve_public_key(jwt_string): - ''' - Gets the public key used to sign the JWT from the public JWK - hosted on Auth0. Auth0 typically only returns one public key - in the JWK set but to handle situations in which signing keys - are being rotated, this method is build to search through - multiple JWK that could be in the set. - - As JWKS are being cached, if a JWK cannot be found, cached - JWKS is purged and a new JWKS is fetched from Auth0. The - fresh JWKS is then searched again for the needed key. - - Returns the key ID if found, otherwise returns None - :param jwt_string: The JWT token as a string - :return: str - ''' - - try: - # Get the JWK - jwks = get_public_keys_from_auth0(refresh=False) - - # Decode the JWTs header component - unverified_header = jwt.get_unverified_header(str(jwt_string)) - - # Check the JWK for the key the JWT was signed with - rsa_key = get_rsa_from_jwks(jwks, unverified_header['kid']) - if not rsa_key: - logger.debug('No matching key found in JWKS, refreshing') - logger.debug('Unverified JWT key id: {}'.format(unverified_header['kid'])) - logger.debug('Cached JWK keys: {}'.format([jwk['kid'] for jwk in jwks['keys']])) - - # No match found, refresh the jwks - jwks = get_public_keys_from_auth0(refresh=True) - logger.debug('Refreshed JWK keys: {}'.format([jwk['kid'] for jwk in jwks['keys']])) - - # Try it again - rsa_key = get_rsa_from_jwks(jwks, unverified_header['kid']) - if not rsa_key: - logger.warning('No matching key found despite refresh, failing: {}'.format(unverified_header.get('kid'))) - - return rsa_key - - except jwt.exceptions.DecodeError as e: - logger.debug('Invalid JWT used: {}'.format(e)) - - except KeyError as e: - logger.exception('Comparing public key failed: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'jwks': jwks}) - - except Exception as e: - logger.exception('Unexpected public key error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'jwks': jwks}) - - return None - - -def get_rsa_from_jwks(jwks, jwt_kid): - ''' - Searches the JWKS for the signing key used - for the JWT. Returns a dict of the JWK - properties if found, None otherwise. - :param jwks: The set of JWKs from Auth0 - :param jwt_kid: The key ID of the signing key - :return: dict - ''' - # Build the dict containing rsa values - for key in jwks["keys"]: - if key["kid"] == jwt_kid: - rsa_key = { - "kty": key["kty"], - "kid": key["kid"], - "use": key["use"], - "n": key["n"], - "e": key["e"] - } - - return rsa_key - - # No matching key found, must refresh JWT keys - return None - - -def validate_rs256_jwt(jwt_string): - ''' - Verifies the given RS256 JWT. Returns the payload - if verified, otherwise returns None. - :param jwt_string: JWT as a string - :return: dict - ''' - - rsa_pub_key = retrieve_public_key(jwt_string) - payload = None - - if rsa_pub_key: - jwk_key = jwk.JWK(**rsa_pub_key) - - # Iterate each possible client ID - for client_id in settings.AUTH0_CLIENT_ID_LIST: - - # Attempt to validate the JWT (Checks both expiry and signature) - try: - payload = jwt.decode(jwt_string, - jwk_key.export_to_pem(private_key=False), - algorithms=['RS256'], - leeway=120, - audience=client_id) - return payload - - except jwt.ExpiredSignatureError: - logger.debug("JWT Expired.") - - except jwt.InvalidTokenError: - logger.debug("Invalid JWT Token.") - - except Exception as e: - logger.exception('Unexpected validation error: {}'.format(e), exc_info=True, - extra={'jwt': jwt_string, 'auth0_client_id': client_id}) - - return None - - def jwt_login(request, jwt_payload): """ The user has a valid JWT but needs to log into this app. Do so here and return the status. @@ -486,8 +238,7 @@ def logout_redirect(request): logout(request) # Build the URL - login_url = furl(settings.AUTHENTICATION_LOGIN_URL) - login_url.query.params.add('next', request.build_absolute_uri()) + login_url = furl(login_redirect_url(request, next_url=request.build_absolute_uri())) # Check for branding if hasattr(settings, 'SCIAUTH_BRANDING'): @@ -499,7 +250,7 @@ def logout_redirect(request): # Set the URL and purge cookies response = redirect(login_url.url) - response.delete_cookie('DBMI_JWT', domain=settings.COOKIE_DOMAIN) + response.delete_cookie('DBMI_JWT', domain=dbmi_settings.JWT_COOKIE_DOMAIN) logger.debug('Redirecting to: {}'.format(login_url.url)) return response diff --git a/app/hypatio/dbmiauthz_services.py b/app/hypatio/dbmiauthz_services.py index 84b54376..8cf885b1 100644 --- a/app/hypatio/dbmiauthz_services.py +++ b/app/hypatio/dbmiauthz_services.py @@ -2,8 +2,8 @@ from furl import furl import logging -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from dbmi_client.settings import dbmi_settings from projects.models import DataProject from projects.models import Participant @@ -13,10 +13,10 @@ class DBMIAuthz: - user_permissions_url = settings.AUTHZ_BASE + "/user_permission/" - create_profile_permission_url = settings.AUTHZ_BASE + "/user_permission/create_registration_permission_record/" - create_view_permission_url = settings.AUTHZ_BASE + "/user_permission/create_item_view_permission_record/" - remove_view_permission_url = settings.AUTHZ_BASE + "/user_permission/remove_item_view_permission_record/" + user_permissions_url = dbmi_settings.AUTHZ_URL + "/user_permission/" + create_profile_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/create_registration_permission_record/" + create_view_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/create_item_view_permission_record/" + remove_view_permission_url = dbmi_settings.AUTHZ_URL + "/user_permission/remove_item_view_permission_record/" @classmethod def _permissions_query(cls, request, email=None, item=None, search=None): diff --git a/app/hypatio/sciauthz_services.py b/app/hypatio/sciauthz_services.py index bde7c2ed..780d2a32 100644 --- a/app/hypatio/sciauthz_services.py +++ b/app/hypatio/sciauthz_services.py @@ -5,7 +5,7 @@ import furl import logging -from django.conf import settings +from dbmi_client.settings import dbmi_settings from projects.models import DataProject @@ -16,17 +16,12 @@ class SciAuthZ: JWT_HEADERS = None CURRENT_USER_EMAIL = None - def __init__(self, authz_base, jwt, user_email): + def __init__(self, jwt, user_email): - user_permissions_url = authz_base + "/user_permission/" - create_profile_permission = authz_base + "/user_permission/create_registration_permission_record/" - create_view_permission = authz_base + "/user_permission/create_item_view_permission_record/" - remove_view_permission = authz_base + "/user_permission/remove_item_view_permission_record/" - - self.USER_PERMISSIONS_URL = user_permissions_url - self.CREATE_PROFILE_PERMISSION = create_profile_permission - self.CREATE_ITEM_PERMISSION = create_view_permission - self.REMOVE_ITEM_PERMISSION = remove_view_permission + self.USER_PERMISSIONS_URL = dbmi_settings.AUTHZ_URL + "/user_permission/" + self.CREATE_PROFILE_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/create_registration_permission_record/" + self.CREATE_ITEM_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/create_item_view_permission_record/" + self.REMOVE_ITEM_PERMISSION = dbmi_settings.AUTHZ_URL + "/user_permission/remove_item_view_permission_record/" jwt_headers = {"Authorization": "JWT " + jwt, 'Content-Type': 'application/json'} @@ -45,7 +40,7 @@ def user_has_manage_permission(self, item): permissions_url.query.params.add('search', 'Hypatio,MANAGE') try: - user_permissions = requests.get(permissions_url.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() + user_permissions = requests.get(permissions_url.url, headers=self.JWT_HEADERS).json() except JSONDecodeError: user_permissions = None @@ -74,7 +69,6 @@ def current_user_permissions(self): user_permissions_request = requests.get( authz_url.url, headers=self.JWT_HEADERS, - verify=settings.VERIFY_REQUESTS ).json() # If there are any permissions returned, add them to the list. @@ -108,7 +102,6 @@ def create_profile_permission(self, grantee_email, project): self.CREATE_PROFILE_PERMISSION, headers=modified_headers, data=data, - verify=settings.VERIFY_REQUESTS ) return profile_permission @@ -124,7 +117,7 @@ def create_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) + view_permission = requests.post(self.CREATE_ITEM_PERMISSION, headers=modified_headers, data=context) return view_permission def remove_view_permission(self, project, grantee_email): @@ -138,7 +131,7 @@ def remove_view_permission(self, project, grantee_email): "item": 'Hypatio.' + project } - view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context, verify=settings.VERIFY_REQUESTS) + view_permission = requests.post(self.REMOVE_ITEM_PERMISSION, headers=modified_headers, data=context) return view_permission def user_has_single_permission(self, permission, value, email=None): @@ -151,7 +144,7 @@ def user_has_single_permission(self, permission, value, email=None): f.args["email"] = email try: - user_permissions = requests.get(f.url, headers=self.JWT_HEADERS, verify=settings.VERIFY_REQUESTS).json() + user_permissions = requests.get(f.url, headers=self.JWT_HEADERS).json() except JSONDecodeError: logger.debug("[SCIAUTHZ][user_has_single_permission] - No Valid permissions returned.") return False @@ -217,7 +210,6 @@ def get_all_view_permissions_for_project(self, project): user_permissions_request = requests.get( authz_url, headers=self.JWT_HEADERS, - verify=settings.VERIFY_REQUESTS ).json() # If there are any permissions returned, add them to the list. diff --git a/app/hypatio/scireg_services.py b/app/hypatio/scireg_services.py index 587231e7..93e1c5e2 100644 --- a/app/hypatio/scireg_services.py +++ b/app/hypatio/scireg_services.py @@ -4,21 +4,20 @@ from furl import furl from json import JSONDecodeError -from django.conf import settings - +from dbmi_client.settings import dbmi_settings import logging logger = logging.getLogger(__name__) -VERIFY_SSL = True - +# Set the base API URL for registration related queries +DBMI_REG_API_URL = furl(dbmi_settings.REG_URL) / "api" / "register" def build_headers_with_jwt(user_jwt): return {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} def send_confirmation_email(user_jwt, current_uri): - send_confirm_email_url = settings.SCIREG_REGISTRATION_URL + 'send_confirmation_email/' + send_confirm_email_url = (DBMI_REG_API_URL / 'send_confirmation_email/').url logger.debug("[HYPATIO][DEBUG][send_confirmation_email] - Sending user confirmation e-mail to " + send_confirm_email_url) @@ -36,7 +35,7 @@ def get_user_email_confirmation_status(user_jwt): Returns True or False. """ - response = requests.get(settings.SCIREG_REGISTRATION_URL, headers=build_headers_with_jwt(user_jwt)) + response = requests.get(DBMI_REG_API_URL.url, headers=build_headers_with_jwt(user_jwt)) try: email_status = response.json()['results'][0]['email_confirmed'] @@ -50,7 +49,7 @@ def get_user_email_confirmation_status(user_jwt): def get_current_user_profile(user_jwt): - f = furl(settings.SCIREG_REGISTRATION_URL) + f = furl(DBMI_REG_API_URL.url) try: profile = requests.get(f.url, headers=build_headers_with_jwt(user_jwt)).json() @@ -63,7 +62,7 @@ def get_current_user_profile(user_jwt): def get_user_profile(user_jwt, email_of_profile, project_key): - f = furl(settings.SCIREG_REGISTRATION_URL) + f = furl(DBMI_REG_API_URL.url) f.args["email"] = email_of_profile f.args["project"] = 'Hypatio.' + project_key @@ -82,7 +81,7 @@ def get_distinct_countries_participating(user_jwt, participants, project_key): containing the unique countries of these participants and a count for each. """ - url = settings.SCIREG_REGISTRATION_URL + 'get_countries/' + url = (DBMI_REG_API_URL / 'get_countries/').url # From a QuerySet of participants, get a list of their emails emails = [participant.user.email for participant in participants] @@ -108,7 +107,7 @@ def get_names(user_jwt, participants, project_key): containing the first and last names of each participant. """ - url = settings.SCIREG_REGISTRATION_URL + 'get_names/' + url = (DBMI_REG_API_URL.url / 'get_names/').url # From a QuerySet of participants, get a list of their emails emails = list(participants.values_list('user__email', flat=True)) diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index e955fa1a..fdd870c4 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -128,25 +128,8 @@ SITE_URL = environment.get_str("SITE_URL", required=True) -AUTH0_DOMAIN = environment.get_str("AUTH0_DOMAIN", required=True) -AUTH0_CLIENT_ID_LIST = environment.get_list("AUTH0_CLIENT_ID_LIST", required=True) -AUTH0_SECRET = environment.get_str("AUTH0_SECRET", required=True) -AUTH0_SUCCESS_URL = environment.get_str("AUTH0_SUCCESS_URL", required=True) -AUTH0_LOGOUT_URL = environment.get_str("AUTH0_LOGOUT_URL", required=True) - AUTHENTICATION_BACKENDS = ['hypatio.auth0authenticate.Auth0Authentication', 'django.contrib.auth.backends.ModelBackend'] -AUTHENTICATION_LOGIN_URL = environment.get_str("ACCOUNT_SERVER_URL", required=True) -ACCOUNT_SERVER_URL = environment.get_str("ACCOUNT_SERVER_URL", required=True) -SCIREG_SERVER_URL = environment.get_str("SCIREG_SERVER_URL", required=True) -AUTHZ_BASE = environment.get_str("AUTHZ_BASE", required=True) - -USER_PERMISSIONS_URL = AUTHZ_BASE + "/user_permission/" - -SCIREG_REGISTRATION_URL = SCIREG_SERVER_URL + "/api/register/" - -COOKIE_DOMAIN = environment.get_str("COOKIE_DOMAIN", required=True) - SSL_SETTING = "https" VERIFY_REQUESTS = True @@ -156,8 +139,6 @@ RECAPTCHA_KEY = environment.get_str('RECAPTCHA_KEY', required=True) RECAPTCHA_CLIENT_ID = environment.get_str('RECAPTCHA_CLIENT_ID', required=True) -EMAIL_CONFIRM_SUCCESS_URL = environment.get_str('EMAIL_CONFIRM_SUCCESS_URL', required=True) - ########## # S3 Configurations S3_BUCKET = environment.get_str('S3_BUCKET', required=True) @@ -234,12 +215,11 @@ 'AUTHZ_ADMIN_GROUP': 'hypatio-admins', 'AUTHZ_ADMIN_PERMISSION': 'ADMIN', 'JWT_COOKIE_DOMAIN': environment.get_str('COOKIE_DOMAIN', required=True), - - # Auth0 - 'AUTH0_TENANT': AUTH0_DOMAIN.lower().replace(".auth0.com", ""), - 'AUTH0_CLIENT_ID': next(iter(AUTH0_CLIENT_ID_LIST)), 'AUTHN_TITLE': 'DBMI Portal', + # Set auth configurations + 'AUTH_CLIENTS': environment.get_dict('AUTH_CLIENTS', required=True), + # Fileservice 'FILESERVICE_URL': environment.get_str('FILESERVICE_API_URL', required=True), 'FILESERVICE_GROUP': environment.get_str('FILESERVICE_GROUP', required=True), @@ -364,8 +344,3 @@ 'release': '1', 'site': 'HYPATIO' } - -try: - from .local_settings import * -except ImportError: - pass diff --git a/app/manage/api.py b/app/manage/api.py index ad18a491..d55539bb 100644 --- a/app/manage/api.py +++ b/app/manage/api.py @@ -57,7 +57,7 @@ def set_dataproject_registration_status(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -101,7 +101,7 @@ def set_dataproject_visible_status(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -145,7 +145,7 @@ def set_dataproject_details(request): project_key = request.POST.get("project_key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -200,7 +200,7 @@ def get_static_agreement_form_html(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -250,7 +250,7 @@ def get_hosted_file_edit_form(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -293,7 +293,7 @@ def get_hosted_file_logs(request): project_key = request.GET.get("project-key") project = get_object_or_404(DataProject, project_key=project_key) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -350,7 +350,7 @@ def process_hosted_file_edit_form_submission(request): project_id = request.POST.get("project") project = get_object_or_404(DataProject, id=project_id) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -393,7 +393,7 @@ def download_signed_form(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -448,7 +448,7 @@ def get_signed_form_status(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -482,7 +482,7 @@ def change_signed_form_status(request): user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -528,7 +528,7 @@ def change_signed_form_status(request): team.save() for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(signed_form.project.project_key, member.user.email) # Remove their VIEW permission @@ -573,7 +573,7 @@ def save_team_comment(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -609,7 +609,7 @@ def set_team_status(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) logger.debug( @@ -652,7 +652,7 @@ def set_team_status(request): # If setting to Active, grant each team member access permissions. if status == "active": for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_view_permission(project_key, member.user.email) # Add permission to Participant @@ -662,7 +662,7 @@ def set_team_status(request): # If setting to Deactivated, revoke each team member's permissions. elif status == "deactivated": for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant @@ -707,7 +707,7 @@ def delete_team(request): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -725,7 +725,7 @@ def delete_team(request): # First revoke all VIEW permissions for member in team.participant_set.all(): - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant @@ -769,7 +769,7 @@ def download_team_submissions(request, project_key, team_leader_email): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -834,7 +834,7 @@ def download_submission(request, fileservice_uuid): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -867,7 +867,7 @@ def export_submissions(request, project_key): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -893,7 +893,7 @@ def download_submissions_export(request, project_key, fileservice_uuid): # Check permissions in SciAuthZ. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -934,7 +934,7 @@ def host_submission(request, fileservice_uuid): submission = get_object_or_404(ChallengeTaskSubmission, uuid=fileservice_uuid) project = submission.challenge_task.data_project - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -986,7 +986,7 @@ def host_submission(request, fileservice_uuid): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: @@ -1041,7 +1041,7 @@ def download_email_list(request): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -1113,7 +1113,7 @@ def grant_view_permission(request, project_key, user_email): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( @@ -1182,7 +1182,7 @@ def remove_view_permission(request, project_key, user_email): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( @@ -1232,7 +1232,7 @@ def sync_view_permissions(request, project_key): # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( diff --git a/app/manage/views.py b/app/manage/views.py index f196b56c..c27d44bc 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -66,7 +66,7 @@ def get_context_data(self, **kwargs): context = super(DataProjectListManageView, self).get_context_data(**kwargs) user_jwt = self.request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, self.request.user.email) + sciauthz = SciAuthZ(user_jwt, self.request.user.email) projects_managed = sciauthz.get_projects_managed_by_user() context['projects'] = projects_managed @@ -99,7 +99,7 @@ def dispatch(self, request, *args, **kwargs): user_jwt = request.COOKIES.get("DBMI_JWT", None) - self.sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + self.sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = self.sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -539,7 +539,7 @@ def manage_team(request, project_key, team_leader, template_name='manage/team.ht user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -671,7 +671,7 @@ def get(self, request, project_key, user_email, *args, **kwargs): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: @@ -704,7 +704,7 @@ def post(self, request, project_key, user_email, *args, **kwargs): user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) + sciauthz = SciAuthZ(user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project_key) if not is_manager: diff --git a/app/profile/views.py b/app/profile/views.py index 7935a322..14e50186 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -2,20 +2,22 @@ import logging import requests -from hypatio.auth0authenticate import user_auth_and_jwt -from hypatio.auth0authenticate import validate_request as validate_jwt -from hypatio.auth0authenticate import logout_redirect - from django.conf import settings from django.contrib import messages from django.contrib.auth import logout from django.http import HttpResponse from django.shortcuts import redirect from django.shortcuts import render +from django.urls import reverse +from furl import furl +from dbmi_client.settings import dbmi_settings +from dbmi_client.authn import logout_redirect_url from hypatio import scireg_services - -from .forms import RegistrationForm +from hypatio.auth0authenticate import user_auth_and_jwt +from hypatio.auth0authenticate import validate_request as validate_jwt +from hypatio.auth0authenticate import logout_redirect +from profile.forms import RegistrationForm # Get an instance of a logger logger = logging.getLogger(__name__) @@ -24,8 +26,7 @@ @user_auth_and_jwt def signout(request): logout(request) - response = redirect(settings.AUTH0_LOGOUT_URL) - response.delete_cookie('DBMI_JWT', domain=settings.COOKIE_DOMAIN) + response = redirect(logout_redirect_url(request, request.build_absolute_uri(reverse("index")))) return response @@ -51,12 +52,13 @@ def update_profile(request): logger.debug('[HYPATIO][DEBUG] Profile form fields submitted: ' + json.dumps(registration_form.cleaned_data)) # Create a new registration with a POST + url = furl(dbmi_settings.REG_URL) / "api" / "register" if registration_form.cleaned_data['id'] == "": - requests.post(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) + requests.post((url / "/").url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data)) # Update an existing registration with a PUT to the specific ID else: - registration_url = settings.SCIREG_REGISTRATION_URL + registration_form.cleaned_data['id'] + '/' - requests.put(registration_url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data), verify=settings.VERIFY_REQUESTS) + url.path.segments.extend([registration_form.cleaned_data['id'], ""]) + requests.put(url.url, headers=jwt_headers, data=json.dumps(registration_form.cleaned_data)) return HttpResponse(200) else: @@ -74,7 +76,8 @@ def profile(request, template_name='profile/profile.html'): jwt_headers = {"Authorization": "JWT " + user_jwt, 'Content-Type': 'application/json'} # Query SciReg to get the user's information - registration_info = requests.get(settings.SCIREG_REGISTRATION_URL, headers=jwt_headers, verify=settings.VERIFY_REQUESTS).json() + url = furl(dbmi_settings.REG_URL) / "api" / "register" / "/" + registration_info = requests.get(url.url, headers=jwt_headers).json() logger.debug('[HYPATIO][DEBUG] Registration info ' + json.dumps(registration_info)) diff --git a/app/projects/api.py b/app/projects/api.py index bdb67dc7..024342c6 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -214,7 +214,7 @@ def leave_team(request): # TODO user does not have permissions to remove their view permission (whether or not it exists) # Remove VIEW permissions on the DataProject - # sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + # sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) # sciauthz.remove_view_permission(project_key, request.user.email) # TODO remove team leader's scireg permissions @@ -296,7 +296,7 @@ def join_team(request): extra=context) # Create record to allow leader access to profile. - sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_profile_permission(team_leader, project_key) return redirect('/projects/' + request.POST.get('project_key') + '/') @@ -417,7 +417,7 @@ def upload_challengetasksubmission_file(request): # Check that user has permissions to be submitting files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) if not sciauthz.user_has_single_permission(project_key, "VIEW", request.user.email): logger.warning(f"[{project_key}][{request.user.email}] No Access") @@ -540,7 +540,7 @@ def delete_challengetasksubmission(request): # Check that user has permissions to be viewing files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) submission_uuid = request.POST.get('submission_uuid') submission = ChallengeTaskSubmission.objects.get(uuid=submission_uuid) diff --git a/app/projects/templatetags/projects_extras.py b/app/projects/templatetags/projects_extras.py index cad682d6..c7403e2d 100644 --- a/app/projects/templatetags/projects_extras.py +++ b/app/projects/templatetags/projects_extras.py @@ -1,13 +1,12 @@ -import os import datetime -import furl +from furl import furl import logging from django import template from django.conf import settings -from django.utils.safestring import mark_safe from django.utils.timezone import utc from django.template.loader import render_to_string +from dbmi_client.authn import login_redirect_url from hypatio.dbmiauthz_services import DBMIAuthz @@ -24,10 +23,7 @@ def get_html_form_file_contents(form_file_path): def get_login_url(current_uri): # Build the login URL - login_url = furl.furl(settings.ACCOUNT_SERVER_URL) - - # Add the next URL - login_url.args.add('next', current_uri) + login_url = furl(login_redirect_url(None, next_url=current_uri)) # Add project, if any project = getattr(settings, 'PROJECT', None) diff --git a/app/projects/views.py b/app/projects/views.py index b263a9b1..ed536bef 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -10,16 +10,14 @@ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import TemplateView + from hypatio.sciauthz_services import SciAuthZ from hypatio.dbmiauthz_services import DBMIAuthz from hypatio.scireg_services import get_current_user_profile from hypatio.scireg_services import get_user_email_confirmation_status - from profile.forms import RegistrationForm - from hypatio.auth0authenticate import public_user_auth_and_jwt from hypatio.auth0authenticate import user_auth_and_jwt - from projects.models import AGREEMENT_FORM_TYPE_EXTERNAL_LINK, TEAM_ACTIVE, TEAM_READY from projects.models import AGREEMENT_FORM_TYPE_STATIC from projects.models import AGREEMENT_FORM_TYPE_MODEL @@ -29,7 +27,6 @@ from projects.models import HostedFile from projects.models import Participant from projects.models import SignedAgreementForm - from projects.panels import SIGNUP_STEP_COMPLETED_STATUS from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.panels import SIGNUP_STEP_FUTURE_STATUS @@ -50,7 +47,7 @@ def signed_agreement_form(request): signed_agreement_form_id = request.GET['signed_form_id'] user_jwt = request.COOKIES.get("DBMI_JWT", None) - sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) + sciauthz = SciAuthZ(user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) project = get_object_or_404(DataProject, project_key=project_key) diff --git a/requirements.in b/requirements.in index 87f62afa..4683a25e 100644 --- a/requirements.in +++ b/requirements.in @@ -16,9 +16,7 @@ djangorestframework<4.0 django-smtp-ssl<2.0 django-q<2.0 furl<3.0 -jwcrypto<2.0 mysqlclient<3.0 -pyjwt<3.0 python-dateutil<3.0 python-magic<2.0 raven<7.0 diff --git a/requirements.txt b/requirements.txt index 71589aef..16af615c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -136,13 +136,7 @@ cryptography==38.0.1 \ --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 - # via - # django-dbmi-client - # jwcrypto -deprecated==1.2.13 \ - --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ - --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d - # via jwcrypto + # via django-dbmi-client django==4.1.2 \ --hash=sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793 \ --hash=sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f @@ -175,9 +169,9 @@ django-countries==7.3.2 \ --hash=sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82 \ --hash=sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9 # via -r requirements.in -django-dbmi-client==0.5.4 \ - --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ - --hash=sha256:e37597b1f0de4f62d1121a1e27eb830291c6e8ef714e8939a8ac9507486192f5 +django-dbmi-client==1.0.1 \ + --hash=sha256:0bbe719dae983c5c80d6d6e8b7787dd78c212f4bc348b552ab7721726711d692 \ + --hash=sha256:39bb62beb6f0c3e08002cb32e43caed407a0e1642db04ddd2ca3e869ce46c629 # via -r requirements.in django-health-check==3.17.0 \ --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ @@ -237,11 +231,6 @@ jmespath==1.0.1 \ # via # boto3 # botocore -jwcrypto==1.4.2 \ - --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b - # via - # -r requirements.in - # django-dbmi-client mysqlclient==2.1.1 \ --hash=sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c \ --hash=sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782 \ @@ -266,9 +255,7 @@ pycparser==2.21 \ pyjwt==2.5.0 \ --hash=sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80 \ --hash=sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b - # via - # -r requirements.in - # django-dbmi-client + # via django-dbmi-client python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 @@ -368,69 +355,3 @@ wcwidth==0.2.5 \ --hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \ --hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83 # via blessed -wrapt==1.14.1 \ - --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ - --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ - --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ - --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ - --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ - --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ - --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ - --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ - --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ - --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ - --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ - --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ - --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ - --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ - --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ - --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ - --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ - --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ - --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ - --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ - --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ - --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ - --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ - --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ - --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ - --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ - --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ - --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ - --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ - --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ - --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ - --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ - --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ - --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ - --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ - --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ - --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ - --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ - --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ - --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ - --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ - --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ - --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ - --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ - --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ - --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ - --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ - --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ - --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ - --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ - --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ - --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ - --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ - --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ - --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ - --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ - --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ - --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ - --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ - --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ - --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ - --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ - --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ - --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af - # via deprecated From 219f75b57a21505b8a873f6e12163ba30ddf065f Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 7 Nov 2022 12:08:52 +0000 Subject: [PATCH 554/613] fix(requirements): Updated Python requirements --- requirements.txt | 90 ++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2790f17c..1c6c1721 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,21 +12,21 @@ asgiref==3.5.2 \ --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 # via django -awscli==1.25.90 \ - --hash=sha256:51341ff0e4b1e93e34254f7585c40d6480034df77d6f198ff26418d4c9afd067 \ - --hash=sha256:ec2fa932bee68fe7b6ba83df2343844a7fd9bb74dc26a98386d185860ff8a913 +awscli==1.27.3 \ + --hash=sha256:3a719932a1d1808347c7ad796916fe99dea0fe7d971909f53e7b6f2680d495a8 \ + --hash=sha256:b76f52822f98e25e3dde497e66ab4b30e2aa9572ac35513a4f376b04940592d0 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.89 \ - --hash=sha256:346f8f0d101a4261dac146a959df18d024feda6431e1d9d84f94efd24d086cae \ - --hash=sha256:d0d8ffcdc10821c4562bc7f935cdd840033bbc342ac0e14b6bdd348b3adf4c04 +boto3==1.26.3 \ + --hash=sha256:7e871c481f88e5b2fc6ac16eb190c95de21efb43ab2d959beacf8b7b096b11d2 \ + --hash=sha256:b81e4aa16891eac7532ce6cc9eb690a8d2e0ceea3bcf44b5c5a1309c2500d35f # via -r requirements.in -botocore==1.27.89 \ - --hash=sha256:238f1dfdb8d8d017c2aea082609a3764f3161d32745900f41bcdcf290d95a048 \ - --hash=sha256:621f5413be8f97712b7e36c1b075a8791d1d1b9971a7ee060cdcdf5e2debf6c1 +botocore==1.29.3 \ + --hash=sha256:100534532b2745f6fa019b79199a8941f04b8168f9d557d0847191455f1f1eed \ + --hash=sha256:ac7986fefe1b9c6323d381c4fdee3845c67fa53eb6c9cf586a8e8a07270dbcfe # via # awscli # boto3 @@ -109,33 +109,33 @@ colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==38.0.1 \ - --hash=sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a \ - --hash=sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f \ - --hash=sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0 \ - --hash=sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407 \ - --hash=sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7 \ - --hash=sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6 \ - --hash=sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153 \ - --hash=sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750 \ - --hash=sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad \ - --hash=sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6 \ - --hash=sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b \ - --hash=sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5 \ - --hash=sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a \ - --hash=sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d \ - --hash=sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d \ - --hash=sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294 \ - --hash=sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0 \ - --hash=sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a \ - --hash=sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac \ - --hash=sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61 \ - --hash=sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013 \ - --hash=sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e \ - --hash=sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb \ - --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ - --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ - --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 +cryptography==38.0.3 \ + --hash=sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d \ + --hash=sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd \ + --hash=sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146 \ + --hash=sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7 \ + --hash=sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436 \ + --hash=sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0 \ + --hash=sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828 \ + --hash=sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b \ + --hash=sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55 \ + --hash=sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36 \ + --hash=sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50 \ + --hash=sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2 \ + --hash=sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a \ + --hash=sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8 \ + --hash=sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0 \ + --hash=sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548 \ + --hash=sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320 \ + --hash=sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748 \ + --hash=sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249 \ + --hash=sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959 \ + --hash=sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f \ + --hash=sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0 \ + --hash=sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd \ + --hash=sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220 \ + --hash=sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c \ + --hash=sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722 # via # django-dbmi-client # jwcrypto @@ -143,9 +143,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==4.1.2 \ - --hash=sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793 \ - --hash=sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f +django==4.1.3 \ + --hash=sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1 \ + --hash=sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5 # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -263,9 +263,9 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pyjwt==2.5.0 \ - --hash=sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80 \ - --hash=sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b +pyjwt==2.6.0 \ + --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \ + --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14 # via # -r requirements.in # django-dbmi-client @@ -280,9 +280,9 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.4 \ - --hash=sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91 \ - --hash=sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174 +pytz==2022.6 \ + --hash=sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427 \ + --hash=sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2 # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ From d7d8c7beb256c2aeb730c4f6c03d85af0b781825 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 10 Nov 2022 10:37:48 -0700 Subject: [PATCH 555/613] DBMISVC-122 - Fixed usage of now removed HttpRequest.is_ajax --- app/contact/views.py | 5 +++-- app/manage/views.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/contact/views.py b/app/contact/views.py index e05905e2..d36cd60f 100644 --- a/app/contact/views.py +++ b/app/contact/views.py @@ -5,6 +5,7 @@ from contact.forms import ContactForm from projects.models import DataProject +from manage.views import is_ajax from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse @@ -57,7 +58,7 @@ def contact_form(request, project_key=None): extra=context) # Check how the request was made. - if request.is_ajax(): + if is_ajax(request): return HttpResponse('SUCCESS', status=200) if success else HttpResponse('ERROR', status=500) else: if success: @@ -73,7 +74,7 @@ def contact_form(request, project_key=None): logger.error("[HYPATIO][ERROR][contact_form] Form is invalid! - " + str(request.user.id)) # Check how the request was made. - if request.is_ajax(): + if is_ajax(request): return HttpResponse('INVALID', status=500) else: messages.error(request, 'An unexpected error occurred, please try again') diff --git a/app/manage/views.py b/app/manage/views.py index f196b56c..ae6e697e 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -39,6 +39,12 @@ # Get an instance of a logger logger = logging.getLogger(__name__) + +def is_ajax(request): + # Returns whether a request is a vanilla ajax request or not + return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + + @method_decorator(user_auth_and_jwt, name='dispatch') class DataProjectListManageView(TemplateView): """ @@ -453,7 +459,7 @@ def team_notification(request, project_key=None): msg.send() # Handle outcome - if request.is_ajax(): + if is_ajax(request): return HttpResponse('SUCCESS', status=200) else: # Set a message. @@ -465,7 +471,7 @@ def team_notification(request, project_key=None): }) # Check how the request was made. - if request.is_ajax(): + if is_ajax(request): return HttpResponse('ERROR', status=500) else: messages.error(request, 'An unexpected error occurred, please try again') @@ -481,7 +487,7 @@ def team_notification(request, project_key=None): }) # Check how the request was made. - if request.is_ajax(): + if is_ajax(request): return HttpResponse(form.errors.as_json(), status=500) else: messages.error(request, 'The form was invalid, please try again') @@ -507,7 +513,7 @@ def team_notification(request, project_key=None): logger.exception(f"Could not determine project", exc_info=True, extra={ 'request': request, }) - if request.is_ajax(): + if is_ajax(request): return HttpResponse('The project could not be determined, cannot send message.', status=500) else: messages.error(request, 'The project could not be determined, cannot send message.') @@ -520,7 +526,7 @@ def team_notification(request, project_key=None): logger.exception(f"Could not determine team leader", exc_info=True, extra={ 'request': request, }) - if request.is_ajax(): + if is_ajax(request): return HttpResponse('The team leader could not be determined, cannot send message.', status=500) else: messages.error(request, 'The team leader could not be determined, cannot send message.') From 14cfb90f5b94ff7c051a69de6611d837db693df4 Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 19 Dec 2022 12:07:13 +0000 Subject: [PATCH 556/613] fix(requirements): Updated Python requirements --- requirements.txt | 108 +++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1c6c1721..e99f25e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,28 +12,28 @@ asgiref==3.5.2 \ --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 # via django -awscli==1.27.3 \ - --hash=sha256:3a719932a1d1808347c7ad796916fe99dea0fe7d971909f53e7b6f2680d495a8 \ - --hash=sha256:b76f52822f98e25e3dde497e66ab4b30e2aa9572ac35513a4f376b04940592d0 +awscli==1.27.32 \ + --hash=sha256:7684ef4a46fce7183b528271198d10e7ad374c5d42210368e753cf4bc8e6209f \ + --hash=sha256:c5863cebf830acfd1ef46151f17a58198e0b0469b9a78c2b3a2d5e528a805fdb # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.26.3 \ - --hash=sha256:7e871c481f88e5b2fc6ac16eb190c95de21efb43ab2d959beacf8b7b096b11d2 \ - --hash=sha256:b81e4aa16891eac7532ce6cc9eb690a8d2e0ceea3bcf44b5c5a1309c2500d35f +boto3==1.26.32 \ + --hash=sha256:672b97a634f3408d455bf94a6dfd59ef0c6150019885bc7107e465f062d58c26 \ + --hash=sha256:e0d6215313b03f09a9a38eccc88c1d3ba9868bcaaeb8b20eeb6d88fc3018b94d # via -r requirements.in -botocore==1.29.3 \ - --hash=sha256:100534532b2745f6fa019b79199a8941f04b8168f9d557d0847191455f1f1eed \ - --hash=sha256:ac7986fefe1b9c6323d381c4fdee3845c67fa53eb6c9cf586a8e8a07270dbcfe +botocore==1.29.32 \ + --hash=sha256:27bc3903f7f8c813efd1605ff13ffdfca2c37dc78cadfa488cfda78fca323deb \ + --hash=sha256:b1a65edca151665a6844bf9f317440e31d8d5e4cbce3477f2661462e20c213b1 # via # awscli # boto3 # s3transfer -certifi==2022.9.24 \ - --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \ - --hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -109,33 +109,33 @@ colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==38.0.3 \ - --hash=sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d \ - --hash=sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd \ - --hash=sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146 \ - --hash=sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7 \ - --hash=sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436 \ - --hash=sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0 \ - --hash=sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828 \ - --hash=sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b \ - --hash=sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55 \ - --hash=sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36 \ - --hash=sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50 \ - --hash=sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2 \ - --hash=sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a \ - --hash=sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8 \ - --hash=sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0 \ - --hash=sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548 \ - --hash=sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320 \ - --hash=sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748 \ - --hash=sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249 \ - --hash=sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959 \ - --hash=sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f \ - --hash=sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0 \ - --hash=sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd \ - --hash=sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220 \ - --hash=sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c \ - --hash=sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722 +cryptography==38.0.4 \ + --hash=sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd \ + --hash=sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db \ + --hash=sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290 \ + --hash=sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744 \ + --hash=sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb \ + --hash=sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d \ + --hash=sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70 \ + --hash=sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b \ + --hash=sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876 \ + --hash=sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083 \ + --hash=sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6 \ + --hash=sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1 \ + --hash=sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00 \ + --hash=sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b \ + --hash=sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b \ + --hash=sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285 \ + --hash=sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9 \ + --hash=sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0 \ + --hash=sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d \ + --hash=sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2 \ + --hash=sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8 \ + --hash=sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee \ + --hash=sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b \ + --hash=sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7 \ + --hash=sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353 \ + --hash=sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c # via # django-dbmi-client # jwcrypto @@ -143,9 +143,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==4.1.3 \ - --hash=sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1 \ - --hash=sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5 +django==4.1.4 \ + --hash=sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148 \ + --hash=sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -165,15 +165,15 @@ django-bootstrap-datepicker-plus==3.0.5 \ --hash=sha256:490058eba99d47f48a7d24fa78581c0e36375bdc7aa9605783eeb170d51fd0df \ --hash=sha256:a8bc19cc6846f97ff1e6c447f4e0387881d16e8afa1e8bd7a652c19e545c566b # via -r requirements.in -django-bootstrap3==22.1 \ - --hash=sha256:6ac3af76ec81df53d81f01a19cac1565ca24d5b036a0e636815120a212270d14 \ - --hash=sha256:bab5113b0c865dd644d317d73ae2c3b716215b1bcfacc671124f9dffb823246a +django-bootstrap3==22.2 \ + --hash=sha256:537b08748ab40a9f214968c188ae26cfeadb7987b784f2857396a33b477fa10a \ + --hash=sha256:c128452497500188052c0e0a24fe5639ee1a26170985b8da636d8fea9114430a # via # -r requirements.in # django-dbmi-client -django-countries==7.4.2 \ - --hash=sha256:2eadbe91578fe7b5a0c7782c07487b2ceb4bde9bc5c76eb43458929969f25c62 \ - --hash=sha256:9d553531de8eaf384fec7fdd5867f3cb9b922d384984c3e613b6b29031e5c3c2 +django-countries==7.5 \ + --hash=sha256:5097d9c16eb5f8a8c195f55e647a1cf1ce8a88fdeb27b104de089424013845a6 \ + --hash=sha256:979676b1147ebbc10e8cdd67857ffffbcba8d7a92abf7ca70696ecd57d8f3d4f # via -r requirements.in django-dbmi-client==0.5.4 \ --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ @@ -280,9 +280,9 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.6 \ - --hash=sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427 \ - --hash=sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2 +pytz==2022.7 \ + --hash=sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a \ + --hash=sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ @@ -358,9 +358,9 @@ typing-extensions==4.4.0 \ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via django-countries -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 +urllib3==1.26.13 \ + --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc \ + --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8 # via # botocore # requests From b47cce003cabfd38f87d6a6d51b6549d3cbd7004 Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 2 Jan 2023 12:07:30 +0000 Subject: [PATCH 557/613] fix(requirements): Updated Python requirements --- requirements.txt | 87 +++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/requirements.txt b/requirements.txt index e99f25e8..30e1a15b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,25 +8,25 @@ arrow==1.2.3 \ --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via django-q -asgiref==3.5.2 \ - --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ - --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 +asgiref==3.6.0 \ + --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \ + --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506 # via django -awscli==1.27.32 \ - --hash=sha256:7684ef4a46fce7183b528271198d10e7ad374c5d42210368e753cf4bc8e6209f \ - --hash=sha256:c5863cebf830acfd1ef46151f17a58198e0b0469b9a78c2b3a2d5e528a805fdb +awscli==1.27.41 \ + --hash=sha256:11c015c6ae03ca10068bbd9392cd491478b616d0c5e14f77a1c23dfbb0ef758b \ + --hash=sha256:e94d3f04a5f3fc7cc91c8004ba00e71c7aabd9685c092bd7b2f99a51c55ec0b9 # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.26.32 \ - --hash=sha256:672b97a634f3408d455bf94a6dfd59ef0c6150019885bc7107e465f062d58c26 \ - --hash=sha256:e0d6215313b03f09a9a38eccc88c1d3ba9868bcaaeb8b20eeb6d88fc3018b94d +boto3==1.26.41 \ + --hash=sha256:05a5ce3af2d7419e39d93498c7f56fd5c2cc17870c92c4abc75659553b0b16de \ + --hash=sha256:8cbea352f28ec6b241f348356bcb8f331fc433bec3ad76ebf6194227f1a7f613 # via -r requirements.in -botocore==1.29.32 \ - --hash=sha256:27bc3903f7f8c813efd1605ff13ffdfca2c37dc78cadfa488cfda78fca323deb \ - --hash=sha256:b1a65edca151665a6844bf9f317440e31d8d5e4cbce3477f2661462e20c213b1 +botocore==1.29.41 \ + --hash=sha256:78761227d986d393956b6d08fdadcfe142748828e0e9db33f2f4c42a482dcd35 \ + --hash=sha256:b670b7f8958a2908167081efb6ea39794bf61d618be729984629a63d85cf8bfe # via # awscli # boto3 @@ -109,33 +109,30 @@ colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==38.0.4 \ - --hash=sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd \ - --hash=sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db \ - --hash=sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290 \ - --hash=sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744 \ - --hash=sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb \ - --hash=sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d \ - --hash=sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70 \ - --hash=sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b \ - --hash=sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876 \ - --hash=sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083 \ - --hash=sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6 \ - --hash=sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1 \ - --hash=sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00 \ - --hash=sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b \ - --hash=sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b \ - --hash=sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285 \ - --hash=sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9 \ - --hash=sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0 \ - --hash=sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d \ - --hash=sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2 \ - --hash=sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8 \ - --hash=sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee \ - --hash=sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b \ - --hash=sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7 \ - --hash=sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353 \ - --hash=sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c +cryptography==39.0.0 \ + --hash=sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b \ + --hash=sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f \ + --hash=sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190 \ + --hash=sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f \ + --hash=sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f \ + --hash=sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb \ + --hash=sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c \ + --hash=sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773 \ + --hash=sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72 \ + --hash=sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8 \ + --hash=sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717 \ + --hash=sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9 \ + --hash=sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856 \ + --hash=sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96 \ + --hash=sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288 \ + --hash=sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39 \ + --hash=sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e \ + --hash=sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce \ + --hash=sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1 \ + --hash=sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de \ + --hash=sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df \ + --hash=sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf \ + --hash=sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458 # via # django-dbmi-client # jwcrypto @@ -143,9 +140,9 @@ deprecated==1.2.13 \ --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d # via jwcrypto -django==4.1.4 \ - --hash=sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148 \ - --hash=sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b +django==4.1.5 \ + --hash=sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763 \ + --hash=sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -204,9 +201,9 @@ django-q==1.3.9 \ django-smtp-ssl==1.0 \ --hash=sha256:282863d5905e03686b6555ac788aa732842695d6f9cf1dcfa66d898abb7565d0 # via -r requirements.in -django-storages==1.13.1 \ - --hash=sha256:3540b45618b04be2c867c0982e8d2bd8e34f84dae922267fcebe4691fb93daf0 \ - --hash=sha256:b3d98ecc09f1b1627c2b2cf430964322ce4e08617dbf9b4236c16a32875a1e0b +django-storages==1.13.2 \ + --hash=sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3 \ + --hash=sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688 # via -r requirements.in django-stronghold==0.4.0 \ --hash=sha256:4127d5f9c11f6582a1c03e7758256b1fe5c872f64f212980e5ad5c67f5eeaa3d From a611b3d1ddac842739cad1b4d310986feee3d3ca Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sat, 14 Jan 2023 08:30:08 -0700 Subject: [PATCH 558/613] DBMISVC-HOTFIX-011423 - Updated base image version for Fargate compatibility --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c596f2c3..ec9dfa65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 AS builder +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 AS builder # Install requirements RUN apt-get update \ @@ -19,7 +19,7 @@ RUN pip install -U wheel \ && pip wheel -r /requirements.txt \ --wheel-dir=/root/wheels -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels From eb90a5b1619d67ab024ab8eceec4dbb02cea2999 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Sun, 15 Jan 2023 16:48:10 -0700 Subject: [PATCH 559/613] DBMISVC-HOTFIX-011523 - Updated base image for Fargate compatibility --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c596f2c3..ec9dfa65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 AS builder +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 AS builder # Install requirements RUN apt-get update \ @@ -19,7 +19,7 @@ RUN pip install -U wheel \ && pip wheel -r /requirements.txt \ --wheel-dir=/root/wheels -FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.4.0 +FROM hmsdbmitc/dbmisvc:debian11-slim-python3.10-0.5.0 # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels From edf72bc6b0bf83084a2f254d34385edc0c70baf5 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 30 Jan 2023 12:58:12 -0700 Subject: [PATCH 560/613] DBMISVC-120 - Updated requirements --- requirements.txt | 316 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 237 insertions(+), 79 deletions(-) diff --git a/requirements.txt b/requirements.txt index 16af615c..98723e93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,32 +8,32 @@ arrow==1.2.3 \ --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via django-q -asgiref==3.5.2 \ - --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ - --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 +asgiref==3.6.0 \ + --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \ + --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506 # via django -awscli==1.25.87 \ - --hash=sha256:479556c396e7e77a67463e543f3e32ae41ac268ae7f6a788165f3886c58d8d70 \ - --hash=sha256:e2ba3a1dc593fdcf12bf2ff3f150ec731229d643f6e9b4cedb6b01401c2bb5eb +awscli==1.27.59 \ + --hash=sha256:0df545bd6238ef3a9182e7c9218c20edd40a9a31b0ad0450db59891e13cab214 \ + --hash=sha256:739069babd9a493147530488ec5f6219ac098ed73e7e082634974b0132909a5f # via -r requirements.in blessed==1.19.1 \ --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc # via django-q -boto3==1.24.86 \ - --hash=sha256:59e9c39c867b5fd0575a50d8fafdf72c823c5510254a8615dff24f1584408121 \ - --hash=sha256:c1d4e5d73d7720402c6538965efd6df5807492db8a71f30221dc11355d06db05 +boto3==1.26.59 \ + --hash=sha256:34ee771a5cc84c16e75d4b9ef4672f51c2bafdce66ec457bbaac630b37d9cd5e \ + --hash=sha256:7d9cebb507fc96e6eb429621ccb2e731b75e7bbb8d6d9f0cf0c08089ee3c1ab7 # via -r requirements.in -botocore==1.27.86 \ - --hash=sha256:830359c8bf22f2a386431825060ff9666519c032f2ccd4a69f1a702f7195f6c0 \ - --hash=sha256:dcfe1825b84d17cf11653f8bb4bc77c2f172dcec1dc68eee932d68e5e6ed89d5 +botocore==1.29.59 \ + --hash=sha256:5533644ddefaccfaa98460a63eb73e61a46aad019771226d103b1054b0df6103 \ + --hash=sha256:bc75d41c5eecf624a2f9875483135aa78088a50c8d29847793f92756697cfed5 # via # awscli # boto3 # s3transfer -certifi==2022.9.24 \ - --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \ - --hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -101,45 +101,134 @@ cffi==1.15.1 \ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 # via cryptography -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f +charset-normalizer==3.0.1 \ + --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ + --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ + --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ + --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ + --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ + --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ + --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ + --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ + --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ + --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ + --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ + --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ + --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ + --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ + --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ + --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ + --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ + --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ + --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ + --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ + --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ + --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ + --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ + --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ + --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ + --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ + --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ + --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ + --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ + --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ + --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ + --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ + --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ + --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ + --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ + --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ + --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ + --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ + --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ + --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ + --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ + --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ + --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ + --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ + --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ + --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ + --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ + --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ + --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ + --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ + --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ + --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ + --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ + --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ + --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ + --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ + --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ + --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ + --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ + --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ + --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ + --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ + --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ + --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ + --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ + --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ + --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ + --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ + --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ + --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ + --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ + --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ + --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ + --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ + --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ + --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ + --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ + --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ + --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ + --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ + --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ + --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ + --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ + --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ + --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ + --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ + --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ + --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 # via requests colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==38.0.1 \ - --hash=sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a \ - --hash=sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f \ - --hash=sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0 \ - --hash=sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407 \ - --hash=sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7 \ - --hash=sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6 \ - --hash=sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153 \ - --hash=sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750 \ - --hash=sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad \ - --hash=sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6 \ - --hash=sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b \ - --hash=sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5 \ - --hash=sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a \ - --hash=sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d \ - --hash=sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d \ - --hash=sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294 \ - --hash=sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0 \ - --hash=sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a \ - --hash=sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac \ - --hash=sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61 \ - --hash=sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013 \ - --hash=sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e \ - --hash=sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb \ - --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ - --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ - --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 - # via django-dbmi-client -django==4.1.2 \ - --hash=sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793 \ - --hash=sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f +cryptography==39.0.0 \ + --hash=sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b \ + --hash=sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f \ + --hash=sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190 \ + --hash=sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f \ + --hash=sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f \ + --hash=sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb \ + --hash=sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c \ + --hash=sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773 \ + --hash=sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72 \ + --hash=sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8 \ + --hash=sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717 \ + --hash=sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9 \ + --hash=sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856 \ + --hash=sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96 \ + --hash=sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288 \ + --hash=sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39 \ + --hash=sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e \ + --hash=sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce \ + --hash=sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1 \ + --hash=sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de \ + --hash=sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df \ + --hash=sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf \ + --hash=sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458 + # via + # django-dbmi-client + # jwcrypto +deprecated==1.2.13 \ + --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ + --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d + # via jwcrypto +django==4.1.5 \ + --hash=sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763 \ + --hash=sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -159,19 +248,19 @@ django-bootstrap-datepicker-plus==3.0.5 \ --hash=sha256:490058eba99d47f48a7d24fa78581c0e36375bdc7aa9605783eeb170d51fd0df \ --hash=sha256:a8bc19cc6846f97ff1e6c447f4e0387881d16e8afa1e8bd7a652c19e545c566b # via -r requirements.in -django-bootstrap3==22.1 \ - --hash=sha256:6ac3af76ec81df53d81f01a19cac1565ca24d5b036a0e636815120a212270d14 \ - --hash=sha256:bab5113b0c865dd644d317d73ae2c3b716215b1bcfacc671124f9dffb823246a +django-bootstrap3==22.2 \ + --hash=sha256:537b08748ab40a9f214968c188ae26cfeadb7987b784f2857396a33b477fa10a \ + --hash=sha256:c128452497500188052c0e0a24fe5639ee1a26170985b8da636d8fea9114430a # via # -r requirements.in # django-dbmi-client -django-countries==7.3.2 \ - --hash=sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82 \ - --hash=sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9 +django-countries==7.5 \ + --hash=sha256:5097d9c16eb5f8a8c195f55e647a1cf1ce8a88fdeb27b104de089424013845a6 \ + --hash=sha256:979676b1147ebbc10e8cdd67857ffffbcba8d7a92abf7ca70696ecd57d8f3d4f # via -r requirements.in -django-dbmi-client==1.0.1 \ - --hash=sha256:0bbe719dae983c5c80d6d6e8b7787dd78c212f4bc348b552ab7721726711d692 \ - --hash=sha256:39bb62beb6f0c3e08002cb32e43caed407a0e1642db04ddd2ca3e869ce46c629 +django-dbmi-client==0.5.4 \ + --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ + --hash=sha256:e37597b1f0de4f62d1121a1e27eb830291c6e8ef714e8939a8ac9507486192f5 # via -r requirements.in django-health-check==3.17.0 \ --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ @@ -198,9 +287,9 @@ django-q==1.3.9 \ django-smtp-ssl==1.0 \ --hash=sha256:282863d5905e03686b6555ac788aa732842695d6f9cf1dcfa66d898abb7565d0 # via -r requirements.in -django-storages==1.13.1 \ - --hash=sha256:3540b45618b04be2c867c0982e8d2bd8e34f84dae922267fcebe4691fb93daf0 \ - --hash=sha256:b3d98ecc09f1b1627c2b2cf430964322ce4e08617dbf9b4236c16a32875a1e0b +django-storages==1.13.2 \ + --hash=sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3 \ + --hash=sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688 # via -r requirements.in django-stronghold==0.4.0 \ --hash=sha256:4127d5f9c11f6582a1c03e7758256b1fe5c872f64f212980e5ad5c67f5eeaa3d @@ -231,6 +320,9 @@ jmespath==1.0.1 \ # via # boto3 # botocore +jwcrypto==1.4.2 \ + --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b + # via django-dbmi-client mysqlclient==2.1.1 \ --hash=sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c \ --hash=sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782 \ @@ -252,9 +344,9 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pyjwt==2.5.0 \ - --hash=sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80 \ - --hash=sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b +pyjwt==2.6.0 \ + --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \ + --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14 # via django-dbmi-client python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ @@ -267,9 +359,9 @@ python-magic==0.4.27 \ --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 # via -r requirements.in -pytz==2022.4 \ - --hash=sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91 \ - --hash=sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174 +pytz==2022.7.1 \ + --hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \ + --hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a # via djangorestframework pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ @@ -312,9 +404,9 @@ redis==3.5.3 \ --hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 \ --hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 # via django-q -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf # via # -r requirements.in # django-dbmi-client @@ -341,17 +433,83 @@ sqlparse==0.4.3 \ --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \ --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268 # via django -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via django-countries -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 +urllib3==1.26.14 \ + --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ + --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 # via # botocore # requests -wcwidth==0.2.5 \ - --hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \ - --hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83 +wcwidth==0.2.6 \ + --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \ + --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0 # via blessed +wrapt==1.14.1 \ + --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ + --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ + --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ + --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ + --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ + --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ + --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ + --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ + --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ + --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ + --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ + --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ + --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ + --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ + --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ + --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ + --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ + --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ + --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ + --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ + --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ + --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ + --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ + --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ + --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ + --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ + --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ + --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ + --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ + --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ + --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ + --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ + --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ + --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ + --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ + --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ + --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ + --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ + --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ + --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ + --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ + --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ + --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ + --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ + --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ + --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ + --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ + --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ + --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ + --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ + --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ + --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ + --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ + --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ + --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ + --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ + --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ + --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ + --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ + --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ + --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ + --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ + --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ + --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af + # via deprecated From fb23bc46fa281b98e6d72925344dafc2aae747db Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 30 Jan 2023 13:20:13 -0700 Subject: [PATCH 561/613] DBMISVC-120 - Updated dbmi-client to > v1.0 --- requirements.in | 2 +- requirements.txt | 83 +++--------------------------------------------- 2 files changed, 5 insertions(+), 80 deletions(-) diff --git a/requirements.in b/requirements.in index 4683a25e..f81925dd 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ django-autocomplete-light<4.0 django-bootstrap3<23.0 django-bootstrap-datepicker-plus<4.0 django-countries<8.0 -django-dbmi-client<1.0 +django-dbmi-client<2.0 django-health-check<4.0 django-jquery<4.0 django-jsonfield-backport<2.0 diff --git a/requirements.txt b/requirements.txt index 98723e93..9a745f83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -219,13 +219,7 @@ cryptography==39.0.0 \ --hash=sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df \ --hash=sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf \ --hash=sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458 - # via - # django-dbmi-client - # jwcrypto -deprecated==1.2.13 \ - --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ - --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d - # via jwcrypto + # via django-dbmi-client django==4.1.5 \ --hash=sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763 \ --hash=sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef @@ -258,9 +252,9 @@ django-countries==7.5 \ --hash=sha256:5097d9c16eb5f8a8c195f55e647a1cf1ce8a88fdeb27b104de089424013845a6 \ --hash=sha256:979676b1147ebbc10e8cdd67857ffffbcba8d7a92abf7ca70696ecd57d8f3d4f # via -r requirements.in -django-dbmi-client==0.5.4 \ - --hash=sha256:b1c7f6c15d9e96fe34b268a69587a1aa73a012557b45d48e9b43879115a097ec \ - --hash=sha256:e37597b1f0de4f62d1121a1e27eb830291c6e8ef714e8939a8ac9507486192f5 +django-dbmi-client==1.0.2 \ + --hash=sha256:0a5f0040cfdb62a4788176c06b1459c5d9e5fb6afd93ec7f06060a1a65e102fe \ + --hash=sha256:fb49313f564afbbd46994fcaa3e4539a691393dac13aeaeaa4dc5e4b6458f088 # via -r requirements.in django-health-check==3.17.0 \ --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ @@ -320,9 +314,6 @@ jmespath==1.0.1 \ # via # boto3 # botocore -jwcrypto==1.4.2 \ - --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b - # via django-dbmi-client mysqlclient==2.1.1 \ --hash=sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c \ --hash=sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782 \ @@ -447,69 +438,3 @@ wcwidth==0.2.6 \ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0 # via blessed -wrapt==1.14.1 \ - --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ - --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ - --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ - --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ - --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ - --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ - --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ - --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ - --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ - --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ - --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ - --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ - --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ - --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ - --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ - --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ - --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ - --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ - --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ - --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ - --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ - --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ - --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ - --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ - --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ - --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ - --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ - --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ - --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ - --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ - --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ - --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ - --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ - --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ - --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ - --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ - --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ - --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ - --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ - --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ - --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ - --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ - --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ - --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ - --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ - --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ - --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ - --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ - --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ - --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ - --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ - --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ - --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ - --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ - --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ - --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ - --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ - --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ - --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ - --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ - --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ - --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ - --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ - --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af - # via deprecated From acde20ffd6bca00c1903ee97abcbbc6351b02f01 Mon Sep 17 00:00:00 2001 From: b32147 Date: Mon, 13 Feb 2023 12:07:45 +0000 Subject: [PATCH 562/613] fix(requirements): Updated Python requirements --- requirements.txt | 94 ++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9a745f83..a10d6b75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,22 +11,24 @@ arrow==1.2.3 \ asgiref==3.6.0 \ --hash=sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac \ --hash=sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506 - # via django -awscli==1.27.59 \ - --hash=sha256:0df545bd6238ef3a9182e7c9218c20edd40a9a31b0ad0450db59891e13cab214 \ - --hash=sha256:739069babd9a493147530488ec5f6219ac098ed73e7e082634974b0132909a5f + # via + # django + # django-countries +awscli==1.27.69 \ + --hash=sha256:2a8e646277d762f47b150ad7e71dfa88867411658a4943b416f35d9be7be4dd8 \ + --hash=sha256:c8a82d5527e1b6e3468592d8e1b16840fa85d621005bd2ac5d0372f595671f98 # via -r requirements.in -blessed==1.19.1 \ - --hash=sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b \ - --hash=sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc +blessed==1.20.0 \ + --hash=sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058 \ + --hash=sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680 # via django-q -boto3==1.26.59 \ - --hash=sha256:34ee771a5cc84c16e75d4b9ef4672f51c2bafdce66ec457bbaac630b37d9cd5e \ - --hash=sha256:7d9cebb507fc96e6eb429621ccb2e731b75e7bbb8d6d9f0cf0c08089ee3c1ab7 +boto3==1.26.69 \ + --hash=sha256:9a0a29179957cb26fa8c3c1fddf66b18efaeaf633e08db5fb53815ffb0421419 \ + --hash=sha256:eb8cde24a4c5755c35126e8cd460e6b51c63d04292419e7e95721232720c7e5b # via -r requirements.in -botocore==1.29.59 \ - --hash=sha256:5533644ddefaccfaa98460a63eb73e61a46aad019771226d103b1054b0df6103 \ - --hash=sha256:bc75d41c5eecf624a2f9875483135aa78088a50c8d29847793f92756697cfed5 +botocore==1.29.69 \ + --hash=sha256:2a4ab8bcb3177daa425019e125c09996b9a6a1a62bb0baaaeeb86ffd552719cc \ + --hash=sha256:7e1bebca013544fbc298cb58603bfccd5f71b49c720a5c33c07cf5dfc8145a1f # via # awscli # boto3 @@ -195,34 +197,34 @@ colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==39.0.0 \ - --hash=sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b \ - --hash=sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f \ - --hash=sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190 \ - --hash=sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f \ - --hash=sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f \ - --hash=sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb \ - --hash=sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c \ - --hash=sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773 \ - --hash=sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72 \ - --hash=sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8 \ - --hash=sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717 \ - --hash=sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9 \ - --hash=sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856 \ - --hash=sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96 \ - --hash=sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288 \ - --hash=sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39 \ - --hash=sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e \ - --hash=sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce \ - --hash=sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1 \ - --hash=sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de \ - --hash=sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df \ - --hash=sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf \ - --hash=sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458 +cryptography==39.0.1 \ + --hash=sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4 \ + --hash=sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f \ + --hash=sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885 \ + --hash=sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502 \ + --hash=sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41 \ + --hash=sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965 \ + --hash=sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e \ + --hash=sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc \ + --hash=sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad \ + --hash=sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505 \ + --hash=sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388 \ + --hash=sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6 \ + --hash=sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2 \ + --hash=sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef \ + --hash=sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac \ + --hash=sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695 \ + --hash=sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6 \ + --hash=sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336 \ + --hash=sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0 \ + --hash=sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c \ + --hash=sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106 \ + --hash=sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a \ + --hash=sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8 # via django-dbmi-client -django==4.1.5 \ - --hash=sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763 \ - --hash=sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef +django==4.1.6 \ + --hash=sha256:bceb0fe1a386781af0788cae4108622756cd05e7775448deec04a71ddf87685d \ + --hash=sha256:c6fe7ebe7c017fe59f1029821dae0acb5a2ddcd6c9a0138fd20a8bfefac914bc # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -248,13 +250,13 @@ django-bootstrap3==22.2 \ # via # -r requirements.in # django-dbmi-client -django-countries==7.5 \ - --hash=sha256:5097d9c16eb5f8a8c195f55e647a1cf1ce8a88fdeb27b104de089424013845a6 \ - --hash=sha256:979676b1147ebbc10e8cdd67857ffffbcba8d7a92abf7ca70696ecd57d8f3d4f +django-countries==7.5.1 \ + --hash=sha256:22915d9b9403932b731622619940a54894a3eb0da9a374e7249c8fc453c122d7 \ + --hash=sha256:2df707aca7a5e677254bed116cf6021a136ebaccd5c2f46860abd6452bb45521 # via -r requirements.in -django-dbmi-client==1.0.2 \ - --hash=sha256:0a5f0040cfdb62a4788176c06b1459c5d9e5fb6afd93ec7f06060a1a65e102fe \ - --hash=sha256:fb49313f564afbbd46994fcaa3e4539a691393dac13aeaeaa4dc5e4b6458f088 +django-dbmi-client==1.0.3 \ + --hash=sha256:0900bbc4c29125b15d62f1ed4c12eebb26f3d0542575bb4349182c33d08100c5 \ + --hash=sha256:ccd8a19b2f4cc82555311e54d00ea7edc4526744772acaeefacd21f01dfa2891 # via -r requirements.in django-health-check==3.17.0 \ --hash=sha256:20dc5ccb516a4e7163593fd4026f0a7531e3027b47d23ebe3bd9dbc99ac4354c \ From 1ccf72085337fcf9125a06c2d72fe3a6209838c1 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 13 Feb 2023 08:16:50 -0700 Subject: [PATCH 563/613] DBMISVC-HOTFIX-021323 - Updated file upload error message --- app/templates/projects/participate/complete-tasks.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/projects/participate/complete-tasks.html b/app/templates/projects/participate/complete-tasks.html index 675d64db..0b5ff512 100644 --- a/app/templates/projects/participate/complete-tasks.html +++ b/app/templates/projects/participate/complete-tasks.html @@ -149,7 +149,7 @@ // Validate content type if( file.type !== fileInput.data("content-type")) { console.log(`Blob type ${file.type} !== ${fileInput.data("content-type")}`); - error(`Only files of type "${fileInput.data("content-type")}" are accepted`, form); + error(`Content type "${file.type}" is not accepted. Only files of type "${fileInput.data("content-type")}" are accepted.`, form); return true; } From c1354215142b79b3760df498a7bd72f792b72686 Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Mon, 13 Feb 2023 12:33:51 -0500 Subject: [PATCH 564/613] Update index.html --- app/templates/index.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index c10af535..2226689c 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -30,7 +30,7 @@

    Data Sets

    Data Challenges

    -

    The n2c2 challenge series now continues under the stewardship of DBMI, with data access and challenge participation administered through this data portal and additional information provided through the public n2c2 website. Our 2022 n2c2 challenge is now underway. Thanks to all who are participating! Results will be presented later this year at the 2022 AMIA Annual Symposium and subsequently published in special journal issues.

    +

    The n2c2 challenge series now continues under the stewardship of DBMI, with data access and challenge participation administered through this data portal and additional information provided through the public n2c2 website. Our 2022 n2c2 challenge culminated with a workshop at the 2022 AMIA Annual Symposium. Thanks to all who participated! Results of Track 2 on Social Determinants of Health will also be published in a December 2023 special issue of JAMIA.

    -
    Logo for Department of Biomedical Informatics | Blavatnik Institute | Harvard Medical School
    +
    Logo for Department of Biomedical Informatics | Blavatnik Institute | Harvard Medical School


    Department of Biomedical Informatics
    @@ -61,7 +62,7 @@

    About Us


    - ©2022 by the President and Fellows of Harvard College

    + ©2023 by the President and Fellows of Harvard College

    From 8c87a186c0ae92b259842ffb9687c4465fb8e16c Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Mon, 13 Feb 2023 13:07:54 -0500 Subject: [PATCH 565/613] Update index.html --- app/templates/index.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index 2226689c..fb9443be 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -30,7 +30,8 @@

    Data Sets

    Data Challenges

    -

    The n2c2 challenge series now continues under the stewardship of DBMI, with data access and challenge participation administered through this data portal and additional information provided through the public n2c2 website. Our 2022 n2c2 challenge culminated with a workshop at the 2022 AMIA Annual Symposium. Thanks to all who participated! Results of Track 2 on Social Determinants of Health will also be published in a December 2023 special issue of JAMIA.

    +

    The n2c2 challenge series now continues under the stewardship of DBMI, with data access and challenge participation administered through this data portal and additional information provided through the public n2c2 website.

    +

    Our 2022 n2c2 challenge culminated with a workshop at the 2022 AMIA Annual Symposium . Thanks to all who participated! Results of Track 2 on Social Determinants of Health will also be published in a December 2023 special issue of JAMIA .

    About Us

    @@ -43,7 +44,7 @@

    About Us

  • Li Lab — Software & Database
  • Park Lab — Software
  • Patel Group — Resources
  • -
  • Rajpurkar Lab — Datasets and Codebases | MAIDA Initiative
  • +
  • Rajpurkar Lab — Datasets and Codebases | MAIDA Initiative
  • Sunyaev Lab — Software
  • Zitnik Lab — Datasets | ML Tools
  • From 44d8f48b23cc6bf8a31d4acfd41f1d83ff71ec45 Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Mon, 13 Feb 2023 16:37:27 -0500 Subject: [PATCH 566/613] Update index.html no www in liheng.org --- app/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/index.html b/app/templates/index.html index fb9443be..7a859b62 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -41,7 +41,7 @@

    About Us

  • Cai Lab — Software | Web Apps
  • Farhat Lab — Github Repository
  • Gehlenborg Lab — Projects
  • -
  • Li Lab — Software & Database
  • +
  • Li Lab — Software & Database
  • Park Lab — Software
  • Patel Group — Resources
  • Rajpurkar Lab — Datasets and Codebases | MAIDA Initiative
  • From de4eb8f86d7fe687506356e1b99e50a0bd5edcba Mon Sep 17 00:00:00 2001 From: Rachel Eastwood <2087481+reastwood@users.noreply.github.com> Date: Mon, 13 Feb 2023 19:04:54 -0500 Subject: [PATCH 567/613] Update index.html add Therapeutics Data Commons --- app/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/index.html b/app/templates/index.html index 7a859b62..b7df6383 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -46,7 +46,7 @@

    About Us

  • Patel Group — Resources
  • Rajpurkar Lab — Datasets and Codebases | MAIDA Initiative
  • Sunyaev Lab — Software
  • -
  • Zitnik Lab — Datasets | ML Tools
  • +
  • Zitnik Lab — Datasets | ML Tools | Therapeutics Data Commons
  • From c4ee9a7e8af391b4207db76a31c3e637a8016b20 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 13 Feb 2023 18:00:40 -0700 Subject: [PATCH 568/613] HYP-290 - Expanded allowed content-types and added more values for zip files --- app/projects/models.py | 6 +++--- .../projects/participate/complete-tasks.html | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/projects/models.py b/app/projects/models.py index e3b8cae7..e97767fe 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -51,8 +51,8 @@ ) FILES_CONTENT_TYPES = { - FILE_TYPE_ZIP: "application/zip", - FILE_TYPE_PDF: "application/pdf", + FILE_TYPE_ZIP: ["application/zip", "application/x-zip-compressed", "multipart/x-zip", "application/x-zip"], + FILE_TYPE_PDF: ["application/pdf", "application/x-pdf"], } def get_agreement_form_upload_path(instance, filename): @@ -591,7 +591,7 @@ def clean(self): raise ValidationError("Closed time must be a datetime after opened time") @property - def submission_file_content_type(self): + def submission_file_content_types(self): return FILES_CONTENT_TYPES[self.submission_file_type] diff --git a/app/templates/projects/participate/complete-tasks.html b/app/templates/projects/participate/complete-tasks.html index 0b5ff512..c14c20c2 100644 --- a/app/templates/projects/participate/complete-tasks.html +++ b/app/templates/projects/participate/complete-tasks.html @@ -43,7 +43,7 @@

    + data-content-type="{{ task_detail.task.submission_file_content_types|join:";" }}">
    @@ -147,10 +147,13 @@ } // Validate content type - if( file.type !== fileInput.data("content-type")) { - console.log(`Blob type ${file.type} !== ${fileInput.data("content-type")}`); - error(`Content type "${file.type}" is not accepted. Only files of type "${fileInput.data("content-type")}" are accepted.`, form); - return true; + let contentTypes = fileInput.data("content-type").split(";"); + let contentTypesString = `"${contentTypes.join("\", \"")}"`; + console.log(`Checking type "${file.type}" in "${contentTypesString}"`); + if( ! contentTypes.includes(file.type) ) { + console.log(`Blob type ${file.type} not in "${contentTypesString}"`); + error(`Content type "${file.type}" is not accepted. Only files of type "${contentTypesString}" are accepted.`, form); + return false; } // Get the form data. From 856eb2db731f03eda26df1a3a526f1f4417c64ae Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Tue, 14 Feb 2023 08:14:10 -0700 Subject: [PATCH 569/613] HYP-287 - Fixed a participant's permissions not being revoked when a required agreement form's approval is rescinded --- app/manage/api.py | 61 ++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/app/manage/api.py b/app/manage/api.py index d55539bb..9c2e7277 100644 --- a/app/manage/api.py +++ b/app/manage/api.py @@ -514,42 +514,55 @@ def change_signed_form_status(request): email_template='email_signed_form_rejection_notification', extra=context) - # If the user is a participant on a team, then the team status may need to be changed try: + # Fetch participant so we can revoke permissions for them and team, if applicable participant = Participant.objects.get(user=affected_user, project=signed_form.project) team = participant.team - except ObjectDoesNotExist: - participant = None - team = None - # If the team is in an Active status, move the team status down to Ready and remove everyone's VIEW permissions - if team is not None and team.status == "Active": - team.status = "Ready" - team.save() + # If just a participant, remove their view permissions + if not team: - for member in team.participant_set.all(): sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) - sciauthz.remove_view_permission(signed_form.project.project_key, member.user.email) + sciauthz.remove_view_permission(signed_form.project.project_key, affected_user.email) # Remove their VIEW permission - member.permission = None - member.save() + participant.permission = None + participant.save() + + # If the team is in an Active status, move the team status down to Ready and remove everyone's VIEW permissions + elif team is not None and team.status == "Active": + team.status = "Ready" + team.save() + + for member in team.participant_set.all(): + sciauthz = SciAuthZ(request.COOKIES.get("DBMI_JWT", None), request.user.email) + sciauthz.remove_view_permission(signed_form.project.project_key, member.user.email) + + # Remove their VIEW permission + member.permission = None + member.save() - logger.debug('[HYPATIO][change_signed_form_status] Emailing the whole team that their status has been moved to Ready because someone has a pending form') + logger.debug('[HYPATIO][change_signed_form_status] Emailing the whole team that their status has been moved to Ready because someone has a pending form') - # Send an email notification to the team - context = {'status': "ready", - 'reason': 'Your team has been temporarily disabled because of an issue with a team members\' forms. Challenge administrators will resolve this shortly.', - 'project': signed_form.project, - 'site_url': settings.SITE_URL} + # Send an email notification to the team + context = {'status': "ready", + 'reason': 'Your team has been temporarily disabled because of an issue with a team members\' forms. Challenge administrators will resolve this shortly.', + 'project': signed_form.project, + 'site_url': settings.SITE_URL} - # Email list - emails = [member.user.email for member in team.participant_set.all()] + # Email list + emails = [member.user.email for member in team.participant_set.all()] + + email_success = email_send(subject='DBMI Portal - Team Status Changed', + recipients=emails, + email_template='email_new_team_status_notification', + extra=context) + except ObjectDoesNotExist: + logger.error( + f'[HYPATIO][change_signed_form_status] Could not find Participant for ' + f'{affected_user.email} / {signed_form.project.project_key}' + ) - email_success = email_send(subject='DBMI Portal - Team Status Changed', - recipients=emails, - email_template='email_new_team_status_notification', - extra=context) else: logger.debug('[HYPATIO][change_signed_form_status] Given status "' + status + '" not one of allowed statuses.') return HttpResponse(500) From 14683488779d4073609db798607ad0f43254f243 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Wed, 22 Feb 2023 08:58:26 -0700 Subject: [PATCH 570/613] HYP-288 - Created a table for pending approvals; updated DataTables; minor tweaks to Project manage views; added 'created' and 'modified' fields to relevant models --- app/manage/urls.py | 2 + app/manage/views.py | 162 ++++- app/projects/admin.py | 29 +- .../migrations/0055_auto_20180814_1538.py | 6 - ...articipant_created_participant_modified.py | 198 ++++++ app/projects/models.py | 38 +- app/static/css/portal.css | 38 ++ .../datatables/dataTables.bootstrap.min.css | 2 +- .../datatables/dataTables.bootstrap.min.js | 12 +- .../datatables/dataTables.dataTables.min.css | 0 .../datatables/dataTables.dataTables.min.js | 4 + .../datatables/jquery.dataTables.min.js | 168 +---- app/templates/manage/project-base.html | 622 ++++++++++++------ 13 files changed, 869 insertions(+), 412 deletions(-) create mode 100644 app/projects/migrations/0096_participant_created_participant_modified.py create mode 100644 app/static/plugins/datatables/dataTables.dataTables.min.css create mode 100644 app/static/plugins/datatables/dataTables.dataTables.min.js diff --git a/app/manage/urls.py b/app/manage/urls.py index 174f1d1d..8821522c 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -5,6 +5,7 @@ from manage.views import DataProjectManageView from manage.views import manage_team from manage.views import ProjectParticipants +from manage.views import ProjectPendingParticipants from manage.views import team_notification from manage.views import UploadSignedAgreementFormView @@ -59,6 +60,7 @@ re_path(r'^grant-view-permission/(?P[^/]+)/(?P[^/]+)/$', grant_view_permission, name='grant-view-permission'), re_path(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), re_path(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), + re_path(r'^get-project-pending-participants/(?P[^/]+)/$', ProjectPendingParticipants.as_view(), name='get-project-pending-participants'), re_path(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), re_path(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), re_path(r'^(?P[^/]+)/(?P[^/]+)/$', manage_team, name='manage-team'), diff --git a/app/manage/views.py b/app/manage/views.py index 64a98cb7..9ac36c44 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -304,21 +304,13 @@ def get(self, request, project_key, *args, **kwargs): sort_order = ['user__email'] if order_direction == 'asc' else ['-user__email'] elif order_column == 3 and not project.has_teams or order_column == 4 and project.has_teams: sort_order = ['permission', 'user__email'] if order_direction == 'asc' else ['-permission', '-user__email'] + elif order_column == 6 and not project.has_teams or order_column == 7 and project.has_teams: + sort_order = ['modified', 'user__email'] if order_direction == 'asc' else ['-modified', '-user__email'] else: sort_order = ['user__email'] if order_direction == 'asc' else ['-user__email'] - # Get list of SignedAgreementForms for this project so we can hide Participants that have yet to complete at - # least one of the required forms - ready_users = [ - s.user for s in SignedAgreementForm.objects.filter( - Q(agreement_form__in=project.agreement_forms.all()) & - (Q(project=project) | Q(project__shares_agreement_forms=True)) - ).select_related("user") - ] - logger.debug(f"{project.project_key}: {len(ready_users)} Ready Participants") - - # Set queryset - query_set = project.participant_set.filter(user__in=ready_users).order_by(*sort_order) + # Get the entire list of current Project Participants + query_set = project.participant_set.order_by(*sort_order) # Setup paginator paginator = Paginator( @@ -394,6 +386,152 @@ def get(self, request, project_key, *args, **kwargs): }, download_count, upload_count, + participant.modified, + ] + + # If project has teams, add that + if project.has_teams: + participant_row.insert(1, participant.team.team_leader.email.lower() if participant.team and participant.team.team_leader else '') + + participants.append(participant_row) + + # Build DataTables response data + data = { + 'draw': draw, + 'recordsTotal': query_set.count(), + 'recordsFiltered': paginator.count, + 'data': participants, + 'error': None, + } + + return JsonResponse(data=data) + + +@method_decorator(user_auth_and_jwt, name='dispatch') +class ProjectPendingParticipants(View): + + def get(self, request, project_key, *args, **kwargs): + + # Pull the project + try: + project = DataProject.objects.get(project_key=project_key) + except DataProject.NotFound: + logger.exception('DataProject for key "{}" not found'.format(project_key)) + return HttpResponse(status=404) + + # Get needed params + draw = int(request.GET['draw']) + start = int(request.GET['start']) + length = int(request.GET['length']) + order_column = int(request.GET['order[0][column]']) + order_direction = request.GET['order[0][dir]'] + + # Check for a search value + search = request.GET['search[value]'] + + # Check what we're sorting by and in what direction + if order_column == 0: + sort_order = ['user__email'] if order_direction == 'asc' else ['-user__email'] + elif order_column == 3 and not project.has_teams or order_column == 4 and project.has_teams: + sort_order = ['modified', '-user__email'] if order_direction == 'asc' else ['-modified', 'user__email'] + else: + sort_order = ['modified', '-user__email'] if order_direction == 'asc' else ['-modified', 'user__email'] + + # Build the query + + # Firstly, we want users with a created Participant for the project, without a permission + # or specifically, without access being granted yet + query_set = Participant.objects.filter(Q(project=project, permission__isnull=True)) + + # Iterate agreement forms + for agreement_form in project.agreement_forms.all(): + + # Filter by the presence of this agreement form in either a pending or accepted state + agreement_form_query = Q( + user__signedagreementform__agreement_form=agreement_form, + user__signedagreementform__status__in=["A", "P"], + ) + + # Ensure the agreement form is for this project or a project that shares agreement forms + agreement_form_query &= ( + Q(user__signedagreementform__project=project) | + Q(user__signedagreementform__project__shares_agreement_forms=True) + ) + + # Filter + query_set = query_set.filter(agreement_form_query) + + # We only want distinct Participants belonging to the users query + query_set = query_set.order_by(*sort_order) + + # Setup paginator + paginator = Paginator( + query_set.filter(user__email__icontains=search) if search else query_set, + length + ) + + # Determine page index (1-index) from DT parameters + page = start / length + 1 + participant_page = paginator.page(page) + + participants = [] + for participant in participant_page: + + signed_agreement_forms = [] + signed_accepted_agreement_forms = 0 + + # For each of the available agreement forms for this project, display only latest version completed by the user + for agreement_form in project.agreement_forms.all(): + + # Check if this project uses shared agreement forms + if project.shares_agreement_forms: + + # Fetch without a specific project + signed_form = SignedAgreementForm.objects.filter( + user__email=participant.user.email, + agreement_form=agreement_form, + ).last() + + else: + + # Fetch only for this project + signed_form = SignedAgreementForm.objects.filter( + user__email=participant.user.email, + project=project, + agreement_form=agreement_form + ).last() + + if signed_form is not None: + signed_agreement_forms.append(signed_form) + + # Collect how many forms are approved to craft language for status + if signed_form.status == 'A': + signed_accepted_agreement_forms += 1 + + # Get the last date of the last updated signed agreement form + modified = max([s.modified for s in signed_agreement_forms]) + + # Build the row of the table for this participant + participant_row = [ + participant.user.email.lower(), + 'Access granted' if participant.permission == 'VIEW' else 'No access', + [ + { + 'status': f.status, + 'id': f.id, + 'name': f.agreement_form.short_name, + 'project': f.project.project_key, + } for f in signed_agreement_forms + ], + { + 'access': True if participant.permission == 'VIEW' else False, + 'email': participant.user.email.lower(), + 'signed': signed_accepted_agreement_forms, + 'team': True if project.has_teams else False, + 'required': project.agreement_forms.count() + }, + participant.modified, + modified, ] # If project has teams, add that diff --git a/app/projects/admin.py b/app/projects/admin.py index e8fe6c49..42a66ef4 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -22,47 +22,56 @@ from projects.models import MIMIC3SignedAgreementFormFields class DataProjectAdmin(admin.ModelAdmin): - list_display = ('name', 'project_key', 'informational_only', 'registration_open', 'requires_authorization', 'is_challenge', 'order') + list_display = ('name', 'project_key', 'informational_only', 'registration_open', 'requires_authorization', 'is_challenge', 'order', 'created', 'modified', ) list_filter = ('informational_only', 'registration_open', 'requires_authorization', 'is_challenge') + readonly_fields = ('created', 'modified', ) class AgreementformAdmin(admin.ModelAdmin): - list_display = ('name', 'short_name', 'type', 'form_file_path') + list_display = ('name', 'short_name', 'type', 'form_file_path', 'created', 'modified', ) + readonly_fields = ('created', 'modified', ) class SignedagreementformAdmin(admin.ModelAdmin): - list_display = ('user', 'agreement_form', 'date_signed', 'status') + list_display = ('user', 'agreement_form', 'date_signed', 'status', 'created', 'modified', ) search_fields = ('user__email', ) + readonly_fields = ('created', 'modified', ) class TeamAdmin(admin.ModelAdmin): - list_display = ('team_leader', 'data_project') + list_display = ('team_leader', 'data_project', 'created', 'modified', ) list_filter = ('data_project', ) search_fields = ('data_project__project_key', 'team_leader__email') + readonly_fields = ('created', 'modified', ) class ParticipantAdmin(admin.ModelAdmin): - list_display = ('user', 'project', 'team') + list_display = ('user', 'project', 'team', 'created', 'modified', ) list_filter = ('project', ) search_fields = ('project__project_key', 'team__team_leader__email', 'user__email') + readonly_fields = ('created', 'modified', ) class InstitutionAdmin(admin.ModelAdmin): - list_display = ('name', 'logo_path') + list_display = ('name', 'logo_path', 'created', 'modified', ) + readonly_fields = ('created', 'modified', ) class HostedFileAdmin(admin.ModelAdmin): - list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order') + list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order', 'created', 'modified',) list_filter = ('project', ) search_fields = ('project__project_key', 'file_name', ) + readonly_fields = ('created', 'modified', ) class HostedFileSetAdmin(admin.ModelAdmin): - list_display = ('title', 'project', 'order') + list_display = ('title', 'project', 'order', 'created', 'modified', ) list_filter = ('project', ) + readonly_fields = ('created', 'modified', ) class HostedFileDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'hosted_file', 'download_date') search_fields = ('user__email', ) class ChallengeTaskAdmin(admin.ModelAdmin): - list_display = ('data_project', 'title', 'enabled', 'opened_time', 'closed_time') + list_display = ('data_project', 'title', 'enabled', 'opened_time', 'closed_time', 'created', 'modified', ) + readonly_fields = ('created', 'modified', ) class ChallengeTaskSubmissionAdmin(admin.ModelAdmin): - list_display = ('participant', 'challenge_task', 'upload_date', 'uuid') + list_display = ('participant', 'challenge_task', 'upload_date', 'uuid', ) list_filter = ('participant__project', 'challenge_task') search_fields = ('participant__project__project_key', 'participant__user__email', 'challenge_task__title') diff --git a/app/projects/migrations/0055_auto_20180814_1538.py b/app/projects/migrations/0055_auto_20180814_1538.py index 4b270ab3..dd343624 100644 --- a/app/projects/migrations/0055_auto_20180814_1538.py +++ b/app/projects/migrations/0055_auto_20180814_1538.py @@ -12,12 +12,6 @@ class Migration(migrations.Migration): ] operations = [ - # For some reason, this migration error will occur: - # "Unknown column 'dataprojectsubmission_id' in 'projects_teamsubmissionsdownload_participant_submissions'" - # To resolve this, rename the column before renaming the model. Requires sqlparse library installed. - migrations.RunSQL( - "ALTER TABLE projects_teamsubmissionsdownload_participant_submissions CHANGE COLUMN participantsubmission_id dataprojectsubmission_id char(32);" - ), migrations.RenameModel( old_name='DataProjectSubmission', new_name='ChallengeTaskSubmission', diff --git a/app/projects/migrations/0096_participant_created_participant_modified.py b/app/projects/migrations/0096_participant_created_participant_modified.py new file mode 100644 index 00000000..3eef78bb --- /dev/null +++ b/app/projects/migrations/0096_participant_created_participant_modified.py @@ -0,0 +1,198 @@ +# Generated by Django 4.1.6 on 2023-02-14 17:07 + +from django.db import migrations, models + +from projects.models import AgreementForm, SignedAgreementForm + + +def migrate_agreement_form_model(apps, schema_editor): + """ + Sets the initial value of the created field to the existing value of the + modified field. + """ + for agreement_form in AgreementForm.objects.all(): + + # Set the dates + agreement_form.modified = agreement_form.created + agreement_form.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0095_alter_agreementform_id_alter_challengetask_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='participant', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='participant', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='agreementform', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='challengetask', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='challengetask', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='dataproject', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='dataproject', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='hostedfile', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='hostedfile', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='hostedfileset', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='hostedfileset', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='institution', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='institution', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='team', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='team', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='teamcomment', + name='created', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.AddField( + model_name='teamcomment', + name='modified', + field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), + ), + migrations.RunPython(migrate_agreement_form_model), + migrations.AlterField( + model_name='agreementform', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='challengetask', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='challengetask', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='dataproject', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='dataproject', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='hostedfile', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='hostedfile', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='hostedfileset', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='hostedfileset', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='institution', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='institution', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='participant', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='participant', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='team', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='team', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='teamcomment', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='teamcomment', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index e97767fe..f096aaa8 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -8,6 +8,7 @@ from django.db.models import JSONField from django.core.files.uploadedfile import UploadedFile + TEAM_PENDING = 'Pending' TEAM_READY = 'Ready' TEAM_ACTIVE = 'Active' @@ -86,6 +87,10 @@ class Institution(models.Model): name = models.CharField(max_length=100, blank=False, null=False, verbose_name="name") logo_path = models.CharField(max_length=300, blank=True, null=True) + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s' % (self.name) @@ -102,7 +107,6 @@ class AgreementForm(models.Model): name = models.CharField(max_length=100, blank=False, null=False, verbose_name="name") short_name = models.CharField(max_length=16, blank=False, null=False) description = models.TextField(blank=True) - created = models.DateTimeField(auto_now_add=True) form_file_path = models.CharField(max_length=300, blank=True, null=True) external_link = models.CharField(max_length=300, blank=True, null=True) type = models.CharField(max_length=50, choices=AGREEMENT_FORM_TYPE, blank=True, null=True) @@ -110,6 +114,10 @@ class AgreementForm(models.Model): content = models.TextField(blank=True, null=True, help_text="If Agreement Form type is set to 'MODEL', the HTML set here will be rendered for the user") internal = models.BooleanField(default=False, help_text="Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants") + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s' % (self.name) @@ -179,6 +187,10 @@ class DataProject(models.Model): order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for how the DataProjects should be listed.") + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s' % (self.project_key) @@ -419,6 +431,10 @@ class Team(models.Model): status = models.CharField(max_length=30, choices=TEAM_STATUS, default='Pending') source = models.ForeignKey("Team", null=True, blank=True, on_delete=models.CASCADE) + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + class Meta: unique_together = ('team_leader', 'data_project',) @@ -450,6 +466,10 @@ class Participant(models.Model): team_pending = models.BooleanField(default=False) team_approved = models.BooleanField(default=False) + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + # TODO remove all these? def assign_pending(self, team): self.set_pending() @@ -494,6 +514,10 @@ class HostedFileSet(models.Model): project = models.ForeignKey(DataProject, on_delete=models.CASCADE) order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for file sets to appear within a DataProject.") + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return self.project.project_key + ': ' + self.title @@ -526,6 +550,10 @@ class HostedFile(models.Model): order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for files to appear within a DataProject.") + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s - %s' % (self.project, self.long_name) @@ -550,6 +578,10 @@ class TeamComment(models.Model): date = models.DateTimeField(auto_now_add=True) text = models.CharField(max_length=2000, blank=False, null=False) + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s %s %s' % (self.user, self.team, self.date) @@ -583,6 +615,10 @@ class ChallengeTask(models.Model): # The content type to restrict file uploads to submission_file_type = models.CharField(max_length=15, default=FILE_TYPE_ZIP, choices=FILES_TYPES) + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + def __str__(self): return '%s: %s' % (self.data_project.project_key, self.title) diff --git a/app/static/css/portal.css b/app/static/css/portal.css index 405ddfb4..d46194fc 100644 --- a/app/static/css/portal.css +++ b/app/static/css/portal.css @@ -218,6 +218,8 @@ footer { line-height: 1.5rem; } +/* Bootstrap callout styling */ + .bs-callout { padding: 20px; margin: 20px 0; @@ -290,3 +292,39 @@ footer { .bs-callout-info h4 { color: #5bc0de; } + +/* Bootstrap accordian panels styling */ + +.panel-group { + padding-top: 10px; + } + +.panel-heading.panel-heading-toggle .panel-title:after { + font-family:'Glyphicons Halflings'; + content:"\e113"; + float: right; +} +.panel-heading.panel-heading-toggle.collapsed .panel-title:after { + content:"\e114"; +} + +.panel-heading-toggle { + cursor: pointer; +} + +/* DataTables general styling */ + +.dataTables_processing { + z-index: 999; +} + +table.dataTable.table-hover > tbody > tr:hover > * { + box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.005) !important; +} + +/* Manage views general styling */ + +#files-collapse .panel-body { + max-height: 600px; + overflow: auto; +} diff --git a/app/static/plugins/datatables/dataTables.bootstrap.min.css b/app/static/plugins/datatables/dataTables.bootstrap.min.css index 84ef9532..c8b08862 100644 --- a/app/static/plugins/datatables/dataTables.bootstrap.min.css +++ b/app/static/plugins/datatables/dataTables.bootstrap.min.css @@ -1 +1 @@ -table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} \ No newline at end of file +:root{--dt-row-selected: 0, 136, 204;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{height:1em;width:1em;margin-top:-9px;display:inline-block;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable tr.dt-hasChild td.dt-control:before{content:"-";background-color:#d33333}table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting_asc_disabled,table.dataTable thead>tr>th.sorting_desc_disabled,table.dataTable thead>tr>td.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting_asc_disabled,table.dataTable thead>tr>td.sorting_desc_disabled{cursor:pointer;position:relative;padding-right:26px}table.dataTable thead>tr>th.sorting:before,table.dataTable thead>tr>th.sorting:after,table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_asc:after,table.dataTable thead>tr>th.sorting_desc:before,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>th.sorting_asc_disabled:after,table.dataTable thead>tr>th.sorting_desc_disabled:before,table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting:before,table.dataTable thead>tr>td.sorting:after,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_asc:after,table.dataTable thead>tr>td.sorting_desc:before,table.dataTable thead>tr>td.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_asc_disabled:after,table.dataTable thead>tr>td.sorting_desc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:after{position:absolute;display:block;opacity:.125;right:10px;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.sorting:before,table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_desc:before,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>th.sorting_desc_disabled:before,table.dataTable thead>tr>td.sorting:before,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_desc:before,table.dataTable thead>tr>td.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:before{bottom:50%;content:"▲"}table.dataTable thead>tr>th.sorting:after,table.dataTable thead>tr>th.sorting_asc:after,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>th.sorting_asc_disabled:after,table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting:after,table.dataTable thead>tr>td.sorting_asc:after,table.dataTable thead>tr>td.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc_disabled:after,table.dataTable thead>tr>td.sorting_desc_disabled:after{top:50%;content:"▼"}table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_desc:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting_asc_disabled:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dataTables_scrollBody table.dataTable thead>tr>th:before,div.dataTables_scrollBody table.dataTable thead>tr>th:after,div.dataTables_scrollBody table.dataTable thead>tr>td:before,div.dataTables_scrollBody table.dataTable thead>tr>td:after{display:none}div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:2px}div.dataTables_processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dataTables_processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:0 136 204;animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dataTables_processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1){background-color:transparent}table.dataTable>tbody>tr{background-color:transparent}table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px rgb(0, 136, 204);box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.dataTable>tbody>tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.dataTable.table-striped>tbody>tr.odd>*{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.023)}table.dataTable.table-striped>tbody>tr.odd.selected>*{box-shadow:inset 0 0 0 9999px rgba(0, 136, 204, 0.923);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.923)}table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.075)}table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(0, 136, 204, 0.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975)}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.table-bordered.dataTable{border-right-width:0}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:1px}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^=col-]:last-child{padding-right:0} diff --git a/app/static/plugins/datatables/dataTables.bootstrap.min.js b/app/static/plugins/datatables/dataTables.bootstrap.min.js index c34dce03..3195e492 100644 --- a/app/static/plugins/datatables/dataTables.bootstrap.min.js +++ b/app/static/plugins/datatables/dataTables.bootstrap.min.js @@ -1,8 +1,4 @@ -/*! - DataTables Bootstrap 3 integration - ©2011-2015 SpryMedia Ltd - datatables.net/license -*/ -(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes, - {sWrapper:"dataTables_wrapper form-inline dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; - l=0;for(h=f.length;l",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#", - "aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('

    ",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ja(a),g,n,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ja(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function S(a,b){var c=a.oFeatures,d=c.bFilter; -c.bSort&&lb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;N(a);a._drawHold=!1}function mb(a){var b=a.oClasses,c=h(a.nTable),c=h("
    ").insertBefore(c),d=a.oFeatures,e=h("
    ",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k")[0]; -n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=nb(a);else if("f"==j&&d.bFilter)g=ob(a);else if("r"==j&&d.bProcessing)g=pb(a);else if("t"==j)g=qb(a);else if("i"==j&&d.bInfo)g=rb(a);else if("p"== -j&&d.bPaginate)g=sb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("
    ",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("
    ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Qa(a,h(this).val());N(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function sb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){N(a)}, -b=h("
    ").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;lf&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display", -b?"block":"none");r(a,null,"processing",[a,b])}function qb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("
    ",{"class":f.sScrollWrapper}).append(h("
    ",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ", -{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("
    ",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("
    ",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("
    ",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left", -0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:ka,sName:"scrolling"});return i[0]}function ka(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"), -j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),m=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,T=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),O,K,P,w,Ta=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};K=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==K&&a.scrollBarVis!==k)a.scrollBarVis=K,Y(a);else{a.scrollBarVis=K;p.children("thead, tfoot").remove(); -u&&(P=u.clone().prependTo(p),O=u.find("tr"),P=P.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");K=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=Z(a,b);c.style.width=a.aoColumns[B].sWidth});u&&H(function(a){a.style.width=""},P);f=p.outerWidth();if(""===c){r.width="100%";if(T&&(p.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width= -v(d),f=p.outerWidth());H(C,K);H(function(a){z.push(a.innerHTML);Ta.push(v(h(a).css("width")))},K);H(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ta[b]},o);h(K).height(0);u&&(H(C,P),H(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},P),H(function(a,b){a.style.width=y[b]},O),h(P).height(0));H(function(a,b){a.innerHTML='
    '+z[b]+"
    ";a.style.width=Ta[b]},K);u&&H(function(a,b){a.innerHTML='
    '+ -A[b]+"
    ";a.style.width=y[b]},P);if(p.outerWidth()j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(T&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(O-b);(""===c||""!==d)&&J(a,1,"Possible column misalignment",6)}else O="100%";q.width=v(O);g.width=v(O);u&&(a.nScrollFoot.style.width=v(O));!e&&T&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();n[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+ -(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function H(a,b,c){for(var d=0,e=0,f=b.length,g,j;e").appendTo(j.find("tbody")); -j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ra(a,j.find("thead")[0]);for(m=0;m").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()").css("width",v(a)).appendTo(b||G.body),d=c[0].offsetWidth;c.remove();return d}function Eb(a,b){var c=Fb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("
    ").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Fb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;fd&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;ae?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return ce?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,o=f[b]._aSortData;for(j=0;jg?1:0})}a.bSorted=!0}function Hb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f/g, -"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0e?e+1:3));e=0;for(f=d.length;ee?e+1:3))}a.aLastSort=d}function Gb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=f.length?[0,c[1]]:c)}));b.search!== -k&&h.extend(a.oPreviousSearch,Ab(b.search));if(b.columns){d=0;for(e=b.columns.length;d=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Ma(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ha(a,b){var c=[],c=Kb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0, -c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Wa)},"html-num":function(b){return za(b,a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Wa)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Lb(a){return function(){var b= -[ya(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing= -function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ka(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)}; -this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase(); -return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])}; -this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Lb(e));this.each(function(){var e={},g=1").appendTo(q));p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("
    ",{valign:"top",colSpan:T(t),class:t.oClasses.sRowEmpty}).html(e))[0]}R(t,"aoHeaderCallback","header",[P(t.nTHead).children("tr")[0],ht(t),n,c,u]),R(t,"aoFooterCallback","footer",[P(t.nTFoot).children("tr")[0],ht(t),n,c,u]);s=P(t.nTBody);s.children().detach(),s.append(P(a)),R(t,"aoDrawCallback","draw",[t]),t.bSorted=!1,t.bFiltered=!1,t.bDrawing=!1}}function u(t,e){var n=t.oFeatures,a=n.bSort,n=n.bFilter;a&&ie(t),n?Rt(t,t.oPreviousSearch):t.aiDisplay=t.aiDisplayMaster.slice(),!0!==e&&(t._iDisplayStart=0),t._drawHold=e,v(t),t._drawHold=!1}function _t(t){for(var e,n,a,r,o,i,l,s=t.oClasses,u=P(t.nTable),u=P("
    ").insertBefore(u),c=t.oFeatures,f=P("
    ",{id:t.sTableId+"_wrapper",class:s.sWrapper+(t.nTFoot?"":" "+s.sNoFooter)}),d=(t.nHolding=u[0],t.nTableWrapper=f[0],t.nTableReinsertBefore=t.nTable.nextSibling,t.sDom.split("")),h=0;h")[0],"'"==(r=d[h+1])||'"'==r){for(o="",i=2;d[h+i]!=r;)o+=d[h+i],i++;"H"==o?o=s.sJUIHeader:"F"==o&&(o=s.sJUIFooter),-1!=o.indexOf(".")?(l=o.split("."),a.id=l[0].substr(1,l[0].length-1),a.className=l[1]):"#"==o.charAt(0)?a.id=o.substr(1,o.length-1):a.className=o,h+=i}f.append(a),f=P(a)}else if(">"==n)f=f.parent();else if("l"==n&&c.bPaginate&&c.bLengthChange)e=$t(t);else if("f"==n&&c.bFilter)e=Lt(t);else if("r"==n&&c.bProcessing)e=Zt(t);else if("t"==n)e=Kt(t);else if("i"==n&&c.bInfo)e=Ut(t);else if("p"==n&&c.bPaginate)e=zt(t);else if(0!==C.ext.feature.length)for(var p=C.ext.feature,g=0,b=p.length;g',s=(s=r.sSearch).match(/_INPUT_/)?s.replace("_INPUT_",l):s+l,l=P("
    ",{id:i.f?null:a+"_filter",class:t.sFilter}).append(P("
    ").addClass(t.sLength);return a.aanFeatures.l||(c[0].id=e+"_length"),c.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",l[0].outerHTML)),P("select",c).val(a._iDisplayLength).on("change.DT",function(t){Gt(a,P(this).val()),v(a)}),P(a.nTable).on("length.dt.DT",function(t,e,n){a===e&&P("select",c).val(n)}),c[0]}function zt(t){function c(t){v(t)}var e=t.sPaginationType,f=C.ext.pager[e],d="function"==typeof f,e=P("
    ").addClass(t.oClasses.sPaging+e)[0],h=t.aanFeatures;return d||f.fnInit(t,e,c),h.p||(e.id=t.sTableId+"_paginate",t.aoDrawCallback.push({fn:function(t){if(d)for(var e=t._iDisplayStart,n=t._iDisplayLength,a=t.fnRecordsDisplay(),r=-1===n,o=r?0:Math.ceil(e/n),i=r?1:Math.ceil(a/n),l=f(o,i),s=0,u=h.p.length;s",{id:t.aanFeatures.r?null:t.sTableId+"_processing",class:t.oClasses.sProcessing}).html(t.oLanguage.sProcessing).append("
    ").insertBefore(t.nTable)[0]}function D(t,e){t.oFeatures.bProcessing&&P(t.aanFeatures.r).css("display",e?"block":"none"),R(t,null,"processing",[t,e])}function Kt(t){var e,n,a,r,o,i,l,s,u,c,f,d,h=P(t.nTable),p=t.oScroll;return""===p.sX&&""===p.sY?t.nTable:(e=p.sX,n=p.sY,a=t.oClasses,o=(r=h.children("caption")).length?r[0]._captionSide:null,s=P(h[0].cloneNode(!1)),i=P(h[0].cloneNode(!1)),u=function(t){return t?M(t):null},(l=h.children("tfoot")).length||(l=null),s=P(f="
    ",{class:a.sScrollWrapper}).append(P(f,{class:a.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:e?u(e):"100%"}).append(P(f,{class:a.sScrollHeadInner}).css({"box-sizing":"content-box",width:p.sXInner||"100%"}).append(s.removeAttr("id").css("margin-left",0).append("top"===o?r:null).append(h.children("thead"))))).append(P(f,{class:a.sScrollBody}).css({position:"relative",overflow:"auto",width:u(e)}).append(h)),l&&s.append(P(f,{class:a.sScrollFoot}).css({overflow:"hidden",border:0,width:e?u(e):"100%"}).append(P(f,{class:a.sScrollFootInner}).append(i.removeAttr("id").css("margin-left",0).append("bottom"===o?r:null).append(h.children("tfoot"))))),u=s.children(),c=u[0],f=u[1],d=l?u[2]:null,e&&P(f).on("scroll.DT",function(t){var e=this.scrollLeft;c.scrollLeft=e,l&&(d.scrollLeft=e)}),P(f).css("max-height",n),p.bCollapse||P(f).css("height",n),t.nScrollHead=c,t.nScrollBody=f,t.nScrollFoot=d,t.aoDrawCallback.push({fn:Qt,sName:"scrolling"}),s[0])}function Qt(n){function t(t){(t=t.style).paddingTop="0",t.paddingBottom="0",t.borderTopWidth="0",t.borderBottomWidth="0",t.height=0}var e,a,r,o,i,l=n.oScroll,s=l.sX,u=l.sXInner,c=l.sY,l=l.iBarWidth,f=P(n.nScrollHead),d=f[0].style,h=f.children("div"),p=h[0].style,h=h.children("table"),g=n.nScrollBody,b=P(g),m=g.style,S=P(n.nScrollFoot).children("div"),v=S.children("table"),y=P(n.nTHead),D=P(n.nTable),_=D[0],C=_.style,w=n.nTFoot?P(n.nTFoot):null,T=n.oBrowser,x=T.bScrollOversize,A=(H(n.aoColumns,"nTh"),[]),I=[],F=[],L=[],R=g.scrollHeight>g.clientHeight;n.scrollBarVis!==R&&n.scrollBarVis!==N?(n.scrollBarVis=R,O(n)):(n.scrollBarVis=R,D.children("thead, tfoot").remove(),w&&(R=w.clone().prependTo(D),i=w.find("tr"),a=R.find("tr"),R.find("[id]").removeAttr("id")),R=y.clone().prependTo(D),y=y.find("tr"),e=R.find("tr"),R.find("th, td").removeAttr("tabindex"),R.find("[id]").removeAttr("id"),s||(m.width="100%",f[0].style.width="100%"),P.each(wt(n,R),function(t,e){r=rt(n,t),e.style.width=n.aoColumns[r].sWidth}),w&&k(function(t){t.style.width=""},a),f=D.outerWidth(),""===s?(C.width="100%",x&&(D.find("tbody").height()>g.offsetHeight||"scroll"==b.css("overflow-y"))&&(C.width=M(D.outerWidth()-l)),f=D.outerWidth()):""!==u&&(C.width=M(u),f=D.outerWidth()),k(t,e),k(function(t){var e=j.getComputedStyle?j.getComputedStyle(t).width:M(P(t).width());F.push(t.innerHTML),A.push(e)},e),k(function(t,e){t.style.width=A[e]},y),P(e).css("height",0),w&&(k(t,a),k(function(t){L.push(t.innerHTML),I.push(M(P(t).css("width")))},a),k(function(t,e){t.style.width=I[e]},i),P(a).height(0)),k(function(t,e){t.innerHTML='
    '+F[e]+"
    ",t.childNodes[0].style.height="0",t.childNodes[0].style.overflow="hidden",t.style.width=A[e]},e),w&&k(function(t,e){t.innerHTML='
    '+L[e]+"
    ",t.childNodes[0].style.height="0",t.childNodes[0].style.overflow="hidden",t.style.width=I[e]},a),Math.round(D.outerWidth())g.offsetHeight||"scroll"==b.css("overflow-y")?f+l:f,x&&(g.scrollHeight>g.offsetHeight||"scroll"==b.css("overflow-y"))&&(C.width=M(o-l)),""!==s&&""===u||W(n,1,"Possible column misalignment",6)):o="100%",m.width=M(o),d.width=M(o),w&&(n.nScrollFoot.style.width=M(o)),c||x&&(m.height=M(_.offsetHeight+l)),R=D.outerWidth(),h[0].style.width=M(R),p.width=M(R),y=D.height()>g.clientHeight||"scroll"==b.css("overflow-y"),p[i="padding"+(T.bScrollbarLeft?"Left":"Right")]=y?l+"px":"0px",w&&(v[0].style.width=M(R),S[0].style.width=M(R),S[0].style[i]=y?l+"px":"0px"),D.children("colgroup").insertBefore(D.children("thead")),b.trigger("scroll"),!n.bSorted&&!n.bFiltered||n._drawHold||(g.scrollTop=0))}function k(t,e,n){for(var a,r,o=0,i=0,l=e.length;i/g;function ee(t){var e,n,a=t.nTable,r=t.aoColumns,o=t.oScroll,i=o.sY,l=o.sX,o=o.sXInner,s=r.length,u=it(t,"bVisible"),c=P("th",t.nTHead),f=a.getAttribute("width"),d=a.parentNode,h=!1,p=t.oBrowser,g=p.bScrollOversize,b=a.style.width;for(b&&-1!==b.indexOf("%")&&(f=b),D=0;D").appendTo(b.find("tbody")));for(b.find("thead, tfoot").remove(),b.append(P(t.nTHead).clone()).append(P(t.nTFoot).clone()),b.find("tfoot th, tfoot td").css("width",""),c=wt(t,b.find("thead")[0]),D=0;D").css({width:e.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(t.aoData.length)for(D=0;D").css(l||i?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(b).appendTo(d),y=(l&&o?b.width(o):l?(b.css("width","auto"),b.removeAttr("width"),b.width()").css("width",M(t)).appendTo(e||y.body))[0].offsetWidth,t.remove(),e):0}function re(t,e){var n,a=oe(t,e);return a<0?null:(n=t.aoData[a]).nTr?n.anCells[e]:P("
    ").html(S(t,a,e,"display"))[0]}function oe(t,e){for(var n,a=-1,r=-1,o=0,i=t.aoData.length;oa&&(a=n.length,r=o);return r}function M(t){return null===t?"0px":"number"==typeof t?t<0?"0px":t+"px":t.match(/\d$/)?t+"px":t}function I(t){function e(t){t.length&&!Array.isArray(t[0])?h.push(t):P.merge(h,t)}var n,a,r,o,i,l,s,u=[],c=t.aoColumns,f=t.aaSortingFixed,d=P.isPlainObject(f),h=[];for(Array.isArray(f)&&e(f),d&&f.pre&&e(f.pre),e(t.aaSorting),d&&f.post&&e(f.post),n=0;n/g,""),u=i.nTh;u.removeAttribute("aria-sort"),i=i.bSortable?s+("asc"===(0=o.length?[0,e[1]]:e)})),t.search!==N&&P.extend(n.oPreviousSearch,Bt(t.search)),t.columns){for(a=0,r=t.columns.length;a
    - - - - - - - - - - - {% for team in teams %} - - - - - - - - {% endfor %} - -
    TeamMembersStatusDownloadsSubmissions
    {{ team.team_leader }}{{ team.member_count }} - {% if team.status == "Pending" %} - Pending - {% elif team.status == "Ready" %} - Ready to activate - {% elif team.status == "Active" %} - Active - {% elif team.status == "Deactivated" %} - Deactivated - {% endif %} - {{ team.downloads }}{{ team.submissions }}
    +
    +
    + + + + + + + + + + + + {% for team in teams %} + + + + + + + + {% endfor %} + +
    TeamMembersStatusDownloadsSubmissions
    {{ team.team_leader }}{{ team.member_count }} + {% if team.status == "Pending" %} + Pending + {% elif team.status == "Ready" %} + Ready to activate + {% elif team.status == "Active" %} + Active + {% elif team.status == "Deactivated" %} + Deactivated + {% endif %} + {{ team.downloads }}{{ team.submissions }}
    +
    {% endif %} @@ -322,23 +335,17 @@

    Team list

    -
    -
    - +
    +
    + +
    - +
    @@ -348,8 +355,8 @@

    - - + + @@ -358,6 +365,51 @@

    EmailAccess Forms ActionsDownloadsUploadsRequest DateLast Signed Form Date
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + {% if project.has_teams %} + + {% endif %} + + + + + + + + + + {# Loaded via DataTables.js #} + +
    EmailTeamAccessFormsActionsDownloadsUploadsDate
    +
    +
    +
    @@ -372,57 +424,59 @@

    Submissions management

    - @@ -431,10 +485,10 @@

    Submissions

    - @@ -598,9 +654,6 @@ {% endblock %} {% block footerscripts %} - - - + + {% endblock %} @@ -27,6 +29,7 @@ Registration Visibility Actions + Order @@ -56,6 +59,7 @@ {% endif %} Manage + {%if project.order is not None %}{{ project.order }}{% else %}0{% endif %} {% endfor %} @@ -68,7 +72,24 @@ {% endblock %} diff --git a/app/templates/manage/team.html b/app/templates/manage/team.html index 2602fe85..e0559498 100644 --- a/app/templates/manage/team.html +++ b/app/templates/manage/team.html @@ -4,8 +4,11 @@ {% load tz %} {% block headscripts %} + + + {% endblock %} From def70f480c1aa8254de5cb20971543d3ebf2d115 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 24 Feb 2023 16:00:03 -0700 Subject: [PATCH 572/613] HYP-288 - Fixed pending participant query --- app/manage/views.py | 37 +++++++++++-------- ...articipant_created_participant_modified.py | 19 +++++++++- app/templates/manage/project-base.html | 3 +- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/app/manage/views.py b/app/manage/views.py index 9ac36c44..38ebceaa 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -431,25 +431,22 @@ def get(self, request, project_key, *args, **kwargs): # Check what we're sorting by and in what direction if order_column == 0: - sort_order = ['user__email'] if order_direction == 'asc' else ['-user__email'] + sort_order = ['email'] if order_direction == 'asc' else ['-email'] elif order_column == 3 and not project.has_teams or order_column == 4 and project.has_teams: - sort_order = ['modified', '-user__email'] if order_direction == 'asc' else ['-modified', 'user__email'] + sort_order = ['modified', '-email'] if order_direction == 'asc' else ['-modified', 'email'] else: - sort_order = ['modified', '-user__email'] if order_direction == 'asc' else ['-modified', 'user__email'] + sort_order = ['modified', '-email'] if order_direction == 'asc' else ['-modified', 'email'] # Build the query - # Firstly, we want users with a created Participant for the project, without a permission - # or specifically, without access being granted yet - query_set = Participant.objects.filter(Q(project=project, permission__isnull=True)) - - # Iterate agreement forms + # Find users with all agreement forms approved, but waiting final grant of access + participants_waiting_access = Participant.objects.filter(Q(project=project, permission__isnull=True)) for agreement_form in project.agreement_forms.all(): # Filter by the presence of this agreement form in either a pending or accepted state agreement_form_query = Q( user__signedagreementform__agreement_form=agreement_form, - user__signedagreementform__status__in=["A", "P"], + user__signedagreementform__status="A", ) # Ensure the agreement form is for this project or a project that shares agreement forms @@ -459,10 +456,24 @@ def get(self, request, project_key, *args, **kwargs): ) # Filter - query_set = query_set.filter(agreement_form_query) + participants_waiting_access = participants_waiting_access.filter(agreement_form_query) + + # Secondly, we want Participants with at least one pending SignedAgreementForm + participants_awaiting_approval = Participant.objects.filter(Q(project=project, permission__isnull=True)).filter( + Q( + user__signedagreementform__agreement_form__in=project.agreement_forms.all(), + user__signedagreementform__status="P", + ) & ( + Q(user__signedagreementform__project=project) | + Q(user__signedagreementform__project__shares_agreement_forms=True) + ) + ) # We only want distinct Participants belonging to the users query - query_set = query_set.order_by(*sort_order) + # Django won't sort on a related field after this union so we annotate each queryset with the user's email to sort on + query_set = participants_waiting_access.annotate(email=F("user__email")) \ + .union(participants_awaiting_approval.annotate(email=F("user__email"))) \ + .order_by(*sort_order) # Setup paginator paginator = Paginator( @@ -508,9 +519,6 @@ def get(self, request, project_key, *args, **kwargs): if signed_form.status == 'A': signed_accepted_agreement_forms += 1 - # Get the last date of the last updated signed agreement form - modified = max([s.modified for s in signed_agreement_forms]) - # Build the row of the table for this participant participant_row = [ participant.user.email.lower(), @@ -531,7 +539,6 @@ def get(self, request, project_key, *args, **kwargs): 'required': project.agreement_forms.count() }, participant.modified, - modified, ] # If project has teams, add that diff --git a/app/projects/migrations/0096_participant_created_participant_modified.py b/app/projects/migrations/0096_participant_created_participant_modified.py index 3eef78bb..91ad3033 100644 --- a/app/projects/migrations/0096_participant_created_participant_modified.py +++ b/app/projects/migrations/0096_participant_created_participant_modified.py @@ -2,7 +2,7 @@ from django.db import migrations, models -from projects.models import AgreementForm, SignedAgreementForm +from projects.models import AgreementForm, SignedAgreementForm, Participant def migrate_agreement_form_model(apps, schema_editor): @@ -17,6 +17,22 @@ def migrate_agreement_form_model(apps, schema_editor): agreement_form.save() +def migrate_participants_model(apps, schema_editor): + """ + Attempts to set a roughly accurate date of when each object would have + been created. This is calculated by fetching the date of the last + signed SignedAgreementForm relevant to the DataProjects. + """ + for participant in Participant.objects.all(): + + # Fetch signed agreement forms + signed_agreement_form = SignedAgreementForm.objects.filter(user=participant.user, project=participant.project).last() + + # Set the dates + participant.created = signed_agreement_form.date_signed + participant.modified = signed_agreement_form.date_signed + + class Migration(migrations.Migration): dependencies = [ @@ -110,6 +126,7 @@ class Migration(migrations.Migration): field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), ), migrations.RunPython(migrate_agreement_form_model), + migrations.RunPython(migrate_participants_model), migrations.AlterField( model_name='agreementform', name='modified', diff --git a/app/templates/manage/project-base.html b/app/templates/manage/project-base.html index a1fa33fb..d7468d9c 100644 --- a/app/templates/manage/project-base.html +++ b/app/templates/manage/project-base.html @@ -356,7 +356,6 @@

    Forms Actions Request Date - Last Signed Form Date @@ -778,7 +777,7 @@

    } }, { - "targets": [{% if project.has_teams %}5, 6{% else %}4, 5{% endif %}], + "targets": [{% if project.has_teams %}5{% else %}4{% endif %}], "render": function (data, type, full, meta) { // Return raw dates for sorting From d04535c7bea43147eb94674a7c137cefacea8c25 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 24 Mar 2023 10:28:58 -0600 Subject: [PATCH 573/613] HYP-HOTFIX-032423 - Updated datepicker app; updated requirements --- app/manage/forms.py | 6 +- requirements.in | 2 +- requirements.txt | 294 ++++++++++++++++++++++++-------------------- 3 files changed, 163 insertions(+), 139 deletions(-) diff --git a/app/manage/forms.py b/app/manage/forms.py index 4d142c45..93464f6b 100644 --- a/app/manage/forms.py +++ b/app/manage/forms.py @@ -1,6 +1,6 @@ from django import forms from django.core.validators import RegexValidator -from bootstrap_datepicker_plus import DateTimePickerInput +from bootstrap_datepicker_plus.widgets import DateTimePickerInput from dal import autocomplete from projects.models import AgreementForm, DataProject @@ -52,8 +52,8 @@ class Meta: widgets = { 'long_name': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': '5'}), - 'opened_time': DateTimePickerInput(attrs={'class': 'form-control', 'placeholder': 'MM/DD/YYYY HH:MM'}).start_of('available days'), - 'closed_time': DateTimePickerInput(attrs={'class': 'form-control', 'placeholder': 'MM/DD/YYYY HH:MM'}).end_of('available days'), + 'opened_time': DateTimePickerInput(attrs={'class': 'form-control', 'placeholder': 'MM/DD/YYYY HH:MM'}), + 'closed_time': DateTimePickerInput(attrs={'class': 'form-control', 'placeholder': 'MM/DD/YYYY HH:MM'}, range_from='opened_time'), 'hostedfileset': autocomplete.ModelSelect2(url='projects:hostedfileset-autocomplete', forward=['project'], attrs={'class': 'form-control form-control-select2'}) } diff --git a/requirements.in b/requirements.in index f81925dd..56a0cfa1 100644 --- a/requirements.in +++ b/requirements.in @@ -3,7 +3,7 @@ boto3<2.0 Django<5.0 django-autocomplete-light<4.0 django-bootstrap3<23.0 -django-bootstrap-datepicker-plus<4.0 +django-bootstrap-datepicker-plus<6.0 django-countries<8.0 django-dbmi-client<2.0 django-health-check<4.0 diff --git a/requirements.txt b/requirements.txt index a10d6b75..d8b1fb1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,21 +14,21 @@ asgiref==3.6.0 \ # via # django # django-countries -awscli==1.27.69 \ - --hash=sha256:2a8e646277d762f47b150ad7e71dfa88867411658a4943b416f35d9be7be4dd8 \ - --hash=sha256:c8a82d5527e1b6e3468592d8e1b16840fa85d621005bd2ac5d0372f595671f98 +awscli==1.27.98 \ + --hash=sha256:7d4165a1d0f16d36d08a2aef8d13f9f5de6cc3d9ebd02c045270269de9582c84 \ + --hash=sha256:f6edc2509644d79246287c066f52db00b8f29f4e3e5ee886b3c986cdd9248b5d # via -r requirements.in blessed==1.20.0 \ --hash=sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058 \ --hash=sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680 # via django-q -boto3==1.26.69 \ - --hash=sha256:9a0a29179957cb26fa8c3c1fddf66b18efaeaf633e08db5fb53815ffb0421419 \ - --hash=sha256:eb8cde24a4c5755c35126e8cd460e6b51c63d04292419e7e95721232720c7e5b +boto3==1.26.98 \ + --hash=sha256:983ec9e539431c29b5265e435b91af7c0d77a75809e173427798edb4ede1d69c \ + --hash=sha256:f35a42c6d0130a75e58485efa94383256d9b8c72c3a31ad872807873a8800363 # via -r requirements.in -botocore==1.29.69 \ - --hash=sha256:2a4ab8bcb3177daa425019e125c09996b9a6a1a62bb0baaaeeb86ffd552719cc \ - --hash=sha256:7e1bebca013544fbc298cb58603bfccd5f71b49c720a5c33c07cf5dfc8145a1f +botocore==1.29.98 \ + --hash=sha256:ae906c1feb56063a38ffd2280232fa44d634057825470d3beed274925088cb42 \ + --hash=sha256:b74283ff71eb4e57edfa5cf6dc36d959b1eec618a2b1e5e781643184857dd1c4 # via # awscli # boto3 @@ -103,128 +103,111 @@ cffi==1.15.1 \ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 # via cryptography -charset-normalizer==3.0.1 \ - --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ - --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ - --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ - --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ - --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ - --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ - --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ - --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ - --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ - --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ - --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ - --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ - --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ - --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ - --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ - --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ - --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ - --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ - --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ - --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ - --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ - --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ - --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ - --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ - --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ - --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ - --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ - --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ - --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ - --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ - --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ - --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ - --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ - --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ - --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ - --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ - --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ - --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ - --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ - --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ - --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ - --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ - --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ - --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ - --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ - --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ - --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ - --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ - --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ - --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ - --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ - --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ - --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ - --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ - --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ - --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ - --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ - --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ - --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ - --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ - --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ - --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ - --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ - --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ - --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ - --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ - --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ - --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ - --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ - --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ - --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ - --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ - --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ - --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ - --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ - --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ - --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ - --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ - --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ - --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ - --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ - --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ - --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ - --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ - --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ - --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ - --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ - --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 +charset-normalizer==3.1.0 \ + --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \ + --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \ + --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \ + --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \ + --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \ + --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \ + --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \ + --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \ + --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \ + --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \ + --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \ + --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \ + --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \ + --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \ + --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \ + --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \ + --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \ + --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \ + --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \ + --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \ + --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \ + --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \ + --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \ + --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \ + --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \ + --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \ + --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \ + --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \ + --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \ + --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \ + --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \ + --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \ + --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \ + --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \ + --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \ + --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \ + --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \ + --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \ + --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \ + --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \ + --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \ + --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \ + --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \ + --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \ + --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \ + --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \ + --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \ + --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \ + --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \ + --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \ + --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \ + --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \ + --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \ + --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \ + --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \ + --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \ + --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \ + --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \ + --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \ + --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \ + --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \ + --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \ + --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \ + --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \ + --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \ + --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \ + --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \ + --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \ + --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \ + --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \ + --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \ + --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \ + --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \ + --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \ + --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab # via requests colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via awscli -cryptography==39.0.1 \ - --hash=sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4 \ - --hash=sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f \ - --hash=sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885 \ - --hash=sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502 \ - --hash=sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41 \ - --hash=sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965 \ - --hash=sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e \ - --hash=sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc \ - --hash=sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad \ - --hash=sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505 \ - --hash=sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388 \ - --hash=sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6 \ - --hash=sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2 \ - --hash=sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef \ - --hash=sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac \ - --hash=sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695 \ - --hash=sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6 \ - --hash=sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336 \ - --hash=sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0 \ - --hash=sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c \ - --hash=sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106 \ - --hash=sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a \ - --hash=sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8 +cryptography==40.0.0 \ + --hash=sha256:14da8c26755ffa5c7863ffa5e8b87cb9596a21b6c34852cb19e0f48c226c64fb \ + --hash=sha256:168ded448fb5d82dfa911156ab8b13b1716de65bd50ff977f4657643f998fa05 \ + --hash=sha256:22e63fb48e2615cfab5a9c4bb457d35e7ae03ea8593996bfbe257e78244d12d0 \ + --hash=sha256:23c42c59c2b5b9ddc6a85b5c46b8fabc4d63a1714f4dbea4bf20d25690bf2365 \ + --hash=sha256:34f502619964210939bb7ee7cd5df53178534eb08d3526f941695a8f7aa0efe4 \ + --hash=sha256:43089be365c0ca4235c6e4e781f3bc125bc1fff576c9dd22cdfb585309b9bb9d \ + --hash=sha256:6b36e2864e04c82634879c7e7aad48824b1847fdb06b64cd410d2ec5e51d1b31 \ + --hash=sha256:7162ae4530958114ca2eee30a56eca46527def33493f622f059dc2e825fd0913 \ + --hash=sha256:71cb346b9dd1537102e7466a2d629385b01847f8d96cd7405f0e717d91cebc8e \ + --hash=sha256:722cfddae79684166840be2cbbae154f44a455519e644b60bf274a50ccb834db \ + --hash=sha256:754dc5ab648113dc54197f242db43234a04e4d61193fb5d3ebb42bd569dca571 \ + --hash=sha256:7cc9fc3ffcb766c313ed0515d77d0deabb4f36bdcff3a9f115c43e5ec611b82a \ + --hash=sha256:b05c9f25a1ea42e427085230815bbdebe15a53bb6163c4c06022e5630645046b \ + --hash=sha256:e5855a80c77565fe2464e88e0095764e25d8ddb2d24df2b1d31773e80be94435 \ + --hash=sha256:e917a07094217edeefe8f6ea960b45d7aab650b982e4209da078332cc9d3ac3a \ + --hash=sha256:f2c4134d29cdce0735c16abf48fa8435f001a7b0031e68dd9a9ee1c80a29374a \ + --hash=sha256:f421f6777592eb199ca8abac7c20b9ecef27c50ad63546e6c614b29771b46d0d \ + --hash=sha256:fafa997b9e6818db333ded4b379f5b7679b48bd88ac878428cea2a1aa6e79fd8 \ + --hash=sha256:fba36ec552794a06a07ac8bdc5ad83a587f6959d98547f373d401975d55c7c9e # via django-dbmi-client -django==4.1.6 \ - --hash=sha256:bceb0fe1a386781af0788cae4108622756cd05e7775448deec04a71ddf87685d \ - --hash=sha256:c6fe7ebe7c017fe59f1029821dae0acb5a2ddcd6c9a0138fd20a8bfefac914bc +django==4.1.7 \ + --hash=sha256:44f714b81c5f190d9d2ddad01a532fe502fa01c4cb8faf1d081f4264ed15dcd8 \ + --hash=sha256:f2f431e75adc40039ace496ad3b9f17227022e8b11566f4b363da44c7e44761e # via # -r requirements.in # django-bootstrap-datepicker-plus @@ -240,9 +223,9 @@ django==4.1.6 \ django-autocomplete-light==3.9.4 \ --hash=sha256:0f6da75c1c7186698b867a467a8cdb359f0513fdd8e09288a0c2fb018ae3d94e # via -r requirements.in -django-bootstrap-datepicker-plus==3.0.5 \ - --hash=sha256:490058eba99d47f48a7d24fa78581c0e36375bdc7aa9605783eeb170d51fd0df \ - --hash=sha256:a8bc19cc6846f97ff1e6c447f4e0387881d16e8afa1e8bd7a652c19e545c566b +django-bootstrap-datepicker-plus==5.0.3 \ + --hash=sha256:154bc0234096b420815bfbb7f0534fc6dea7fef7aefc24d5c3a3e961fd2097e3 \ + --hash=sha256:f6e05e19f628d4bcb24524709f22e6dd15950c250aaa2fa2bad8ba828e17228f # via -r requirements.in django-bootstrap3==22.2 \ --hash=sha256:537b08748ab40a9f214968c188ae26cfeadb7987b784f2857396a33b477fa10a \ @@ -337,6 +320,44 @@ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi +pydantic==1.10.7 \ + --hash=sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e \ + --hash=sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6 \ + --hash=sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd \ + --hash=sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca \ + --hash=sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b \ + --hash=sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a \ + --hash=sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245 \ + --hash=sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d \ + --hash=sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee \ + --hash=sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1 \ + --hash=sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3 \ + --hash=sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d \ + --hash=sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5 \ + --hash=sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914 \ + --hash=sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd \ + --hash=sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1 \ + --hash=sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e \ + --hash=sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e \ + --hash=sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a \ + --hash=sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd \ + --hash=sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f \ + --hash=sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209 \ + --hash=sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d \ + --hash=sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a \ + --hash=sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143 \ + --hash=sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918 \ + --hash=sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52 \ + --hash=sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e \ + --hash=sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f \ + --hash=sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e \ + --hash=sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb \ + --hash=sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe \ + --hash=sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe \ + --hash=sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d \ + --hash=sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209 \ + --hash=sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af + # via django-bootstrap-datepicker-plus pyjwt==2.6.0 \ --hash=sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd \ --hash=sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14 @@ -426,13 +447,16 @@ sqlparse==0.4.3 \ --hash=sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34 \ --hash=sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268 # via django -typing-extensions==4.4.0 \ - --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ - --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e - # via django-countries -urllib3==1.26.14 \ - --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ - --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 +typing-extensions==4.5.0 \ + --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ + --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 + # via + # django-bootstrap-datepicker-plus + # django-countries + # pydantic +urllib3==1.26.15 \ + --hash=sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305 \ + --hash=sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42 # via # botocore # requests From 19dfcaabc0b545d1b022109f39d89d4ca02ad571 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 24 Mar 2023 13:53:50 -0600 Subject: [PATCH 574/613] HYP-288 - Fixed migration for Participants model --- ...articipant_created_participant_modified.py | 17 ------- ...articipant_created_participant_modified.py | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 app/projects/migrations/0097_participant_created_participant_modified.py diff --git a/app/projects/migrations/0096_participant_created_participant_modified.py b/app/projects/migrations/0096_participant_created_participant_modified.py index 91ad3033..be54c4e6 100644 --- a/app/projects/migrations/0096_participant_created_participant_modified.py +++ b/app/projects/migrations/0096_participant_created_participant_modified.py @@ -17,22 +17,6 @@ def migrate_agreement_form_model(apps, schema_editor): agreement_form.save() -def migrate_participants_model(apps, schema_editor): - """ - Attempts to set a roughly accurate date of when each object would have - been created. This is calculated by fetching the date of the last - signed SignedAgreementForm relevant to the DataProjects. - """ - for participant in Participant.objects.all(): - - # Fetch signed agreement forms - signed_agreement_form = SignedAgreementForm.objects.filter(user=participant.user, project=participant.project).last() - - # Set the dates - participant.created = signed_agreement_form.date_signed - participant.modified = signed_agreement_form.date_signed - - class Migration(migrations.Migration): dependencies = [ @@ -126,7 +110,6 @@ class Migration(migrations.Migration): field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), ), migrations.RunPython(migrate_agreement_form_model), - migrations.RunPython(migrate_participants_model), migrations.AlterField( model_name='agreementform', name='modified', diff --git a/app/projects/migrations/0097_participant_created_participant_modified.py b/app/projects/migrations/0097_participant_created_participant_modified.py new file mode 100644 index 00000000..e09b65ee --- /dev/null +++ b/app/projects/migrations/0097_participant_created_participant_modified.py @@ -0,0 +1,47 @@ +# Generated by Django 4.1.6 on 2023-02-14 17:07 + +from django.db import migrations, models + +from projects.models import Participant, SignedAgreementForm + + +def migrate_participants_model(apps, schema_editor): + """ + Attempts to set a roughly accurate date of when each object would have + been created. This is calculated by fetching the date of the last + signed SignedAgreementForm relevant to the DataProjects. + """ + for participant in Participant.objects.all(): + + # Fetch signed agreement forms + signed_agreement_form = SignedAgreementForm.objects.filter(user=participant.user, project=participant.project).last() + if signed_agreement_form: + + # Set the dates + participant.created = signed_agreement_form.date_signed + participant.modified = signed_agreement_form.date_signed + + # Do the update + Participant.objects.filter(pk=participant.pk).update( + created=signed_agreement_form.date_signed, + modified=signed_agreement_form.date_signed + ) + + else: + + # Do the update + Participant.objects.filter(pk=participant.pk).update( + created="2018-02-20T00:00:00Z", + modified="2018-02-20T00:00:00Z", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0096_participant_created_participant_modified'), + ] + + operations = [ + migrations.RunPython(migrate_participants_model), + ] From 2ccfcdc1e505ac707eb3069babfc8eae6c63dd1b Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 24 Mar 2023 23:04:14 -0600 Subject: [PATCH 575/613] HYP-292 - Added 4CE challenge; refactored navigation; added Groups to organize DataProjects --- app/hypatio/settings.py | 1 + app/hypatio/urls.py | 2 + app/hypatio/views.py | 36 +++++++++++++- app/projects/admin.py | 8 ++++ .../0098_group_dataproject_group.py | 31 ++++++++++++ app/projects/models.py | 26 ++++++++++ app/projects/views.py | 41 ++++++++++++++++ .../agreementforms/4ce-research-purpose.html | 6 +++ .../4ce-obesity-submissions.html | 47 +++++++++++++++++++ app/templates/base.html | 5 ++ app/templates/projects/group.html | 22 +++++++++ 11 files changed, 224 insertions(+), 1 deletion(-) create mode 100755 app/projects/migrations/0098_group_dataproject_group.py create mode 100644 app/static/agreementforms/4ce-research-purpose.html create mode 100644 app/static/submissionforms/4ce-obesity-submissions.html create mode 100644 app/templates/projects/group.html diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index fdd870c4..e2aaf35a 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -86,6 +86,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'hypatio.views.navigation_context', ], }, }, diff --git a/app/hypatio/urls.py b/app/hypatio/urls.py index 28b9ffef..17ac072a 100644 --- a/app/hypatio/urls.py +++ b/app/hypatio/urls.py @@ -6,6 +6,7 @@ from projects.views import list_data_projects from projects.views import list_data_challenges from projects.views import list_software_projects +from projects.views import GroupView urlpatterns = [ @@ -18,5 +19,6 @@ re_path(r'^data-challenges/$', list_data_challenges, name='data-challenges'), re_path(r'^software-projects/$', list_software_projects, name='software-projects'), re_path(r'^healthcheck/?', include('health_check.urls')), + re_path(r'^groups/(?P[^/]+)/?', GroupView.as_view(), name="group"), re_path(r'^', index, name='index'), ] diff --git a/app/hypatio/views.py b/app/hypatio/views.py index 6da9267e..ebbd2f30 100644 --- a/app/hypatio/views.py +++ b/app/hypatio/views.py @@ -1,7 +1,9 @@ +import os from django.shortcuts import render +from django.utils.functional import SimpleLazyObject from hypatio.auth0authenticate import public_user_auth_and_jwt - +from projects.models import Group, DataProject @public_user_auth_and_jwt def index(request, template_name='index.html'): @@ -12,3 +14,35 @@ def index(request, template_name='index.html'): context = {} return render(request, template_name, context=context) + +def navigation_context(request): + """ + Includes global navigation context in all requests. + + This method is enabled by including it in settings.TEMPLATES as + a context processor. + + :param request: The current HttpRequest + :type request: HttpRequest + :return: The context that should be included in the response's context + :rtype: dict + """ + def group_context(): + + # Check for an active project and determine its group + groups = Group.objects.all() + active_group = None + project = DataProject.objects.filter(project_key=os.path.basename(os.path.normpath(request.path))).first() + if project: + + # Check for group + active_group = next((g for g in groups if project in g.dataproject_set.all()), None) + + return { + "groups": groups, + "active_group": active_group, + } + + return { + "navigation": SimpleLazyObject(group_context) + } diff --git a/app/projects/admin.py b/app/projects/admin.py index 42a66ef4..1beec8cf 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -2,6 +2,7 @@ from django.urls import reverse from django.utils.html import escape, mark_safe +from projects.models import Group from projects.models import DataProject from projects.models import AgreementForm from projects.models import SignedAgreementForm @@ -21,6 +22,12 @@ from projects.models import MAYOSignedAgreementFormFields from projects.models import MIMIC3SignedAgreementFormFields + +class GroupAdmin(admin.ModelAdmin): + list_display = ('title', 'key', 'created', 'modified', ) + readonly_fields = ('created', 'modified', ) + + class DataProjectAdmin(admin.ModelAdmin): list_display = ('name', 'project_key', 'informational_only', 'registration_open', 'requires_authorization', 'is_challenge', 'order', 'created', 'modified', ) list_filter = ('informational_only', 'registration_open', 'requires_authorization', 'is_challenge') @@ -140,6 +147,7 @@ class MIMIC3SignedAgreementFormFieldsAdmin(SignedAgreementFormFieldsAdmin): pass +admin.site.register(Group, GroupAdmin) admin.site.register(DataProject, DataProjectAdmin) admin.site.register(AgreementForm, AgreementformAdmin) admin.site.register(SignedAgreementForm, SignedagreementformAdmin) diff --git a/app/projects/migrations/0098_group_dataproject_group.py b/app/projects/migrations/0098_group_dataproject_group.py new file mode 100755 index 00000000..947cb370 --- /dev/null +++ b/app/projects/migrations/0098_group_dataproject_group.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-03-24 20:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0097_participant_created_participant_modified'), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=100, unique=True)), + ('title', models.CharField(max_length=255)), + ('description', models.TextField(blank=True)), + ('navigation_title', models.CharField(blank=True, max_length=20, null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ], + ), + migrations.AddField( + model_name='dataproject', + name='group', + field=models.ForeignKey(blank=True, help_text='Set this to manage where this project is shown in the navigation and interface.', null=True, on_delete=django.db.models.deletion.PROTECT, to='projects.group'), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index f096aaa8..b5c5107b 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -187,6 +187,14 @@ class DataProject(models.Model): order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for how the DataProjects should be listed.") + group = models.ForeignKey( + to="Group", + on_delete=models.PROTECT, + blank=True, + null=True, + help_text="Set this to manage where this project is shown in the navigation and interface." + ) + # Meta created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -672,3 +680,21 @@ class ChallengeTaskSubmissionDownload(models.Model): user = models.ForeignKey(User, on_delete=models.PROTECT) submission = models.ForeignKey(ChallengeTaskSubmission, on_delete=models.PROTECT) download_date = models.DateTimeField(auto_now_add=True) + + +class Group(models.Model): + """ + An optional grouping for projects. + """ + + key = models.CharField(max_length=100, blank=False, null=False, unique=True) + title = models.CharField(max_length=255, blank=False, null=False) + description = models.TextField(blank=True) + navigation_title = models.CharField(max_length=20, blank=True, null=True) + + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title diff --git a/app/projects/views.py b/app/projects/views.py index ed536bef..7640f190 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -27,6 +27,7 @@ from projects.models import HostedFile from projects.models import Participant from projects.models import SignedAgreementForm +from projects.models import Group from projects.panels import SIGNUP_STEP_COMPLETED_STATUS from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.panels import SIGNUP_STEP_FUTURE_STATUS @@ -113,6 +114,46 @@ def list_software_projects(request, template_name='projects/list-software-projec return render(request, template_name, context=context) +@method_decorator(public_user_auth_and_jwt, name='dispatch') +class GroupView(TemplateView): + """ + Builds and renders screens related to Groups. + """ + + group = None + template_name = 'projects/group.html' + + def dispatch(self, request, *args, **kwargs): + """ + Sets up the instance. + """ + + # Get the project key from the URL. + group_key = self.kwargs['group_key'] + + # If this project does not exist, display a 404 Error. + try: + self.group = Group.objects.get(key=group_key) + except ObjectDoesNotExist: + error_message = "The group you searched for does not exist." + return render(request, '404.html', {'error_message': error_message}) + + return super(GroupView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ + Dynamically builds the context for rendering the view based on information + about the user and the Group. + """ + # Get super's context. This is the dictionary of variables for the base template being rendered. + context = super(GroupView, self).get_context_data(**kwargs) + + # Add the project to the context. + context['group'] = self.group + + return context + + @method_decorator(public_user_auth_and_jwt, name='dispatch') class DataProjectView(TemplateView): """ diff --git a/app/static/agreementforms/4ce-research-purpose.html b/app/static/agreementforms/4ce-research-purpose.html new file mode 100644 index 00000000..2ba163c1 --- /dev/null +++ b/app/static/agreementforms/4ce-research-purpose.html @@ -0,0 +1,6 @@ +
    +
    + + +
    +
    diff --git a/app/static/submissionforms/4ce-obesity-submissions.html b/app/static/submissionforms/4ce-obesity-submissions.html new file mode 100644 index 00000000..79a847b7 --- /dev/null +++ b/app/static/submissionforms/4ce-obesity-submissions.html @@ -0,0 +1,47 @@ +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + +
    +
    + +
    + +
    + +
    diff --git a/app/templates/base.html b/app/templates/base.html index fb545e7e..b392ff55 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -212,6 +212,11 @@ {% url 'data-challenges' as data_challenges_url %} + {% for group in navigation.groups %} + {% url 'group' group.key as group_url %} + + {% endfor %} +