From ea8ab0f80966834220710ad5466434b38fd761cb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 24 Sep 2012 13:28:23 -0400 Subject: [PATCH 0001/1010] Add basic overview.html from tom's wireframe to CMS --- cms/djangoapps/contentstore/views.py | 18 +- cms/static/img/breadcrumb-arrow.png | Bin 0 -> 1700 bytes cms/static/img/date-circle.png | Bin 0 -> 1653 bytes cms/static/img/delete-icon-white.png | Bin 0 -> 970 bytes cms/static/img/delete-icon.png | Bin 0 -> 970 bytes cms/static/img/discussion-module.png | Bin 0 -> 10051 bytes cms/static/img/drag-handles.png | Bin 0 -> 954 bytes cms/static/img/edit-icon-white.png | Bin 0 -> 1067 bytes cms/static/img/edit-icon.png | Bin 0 -> 1066 bytes cms/static/img/expand-collapse-icons.png | Bin 0 -> 1132 bytes cms/static/img/large-discussion-icon.png | Bin 0 -> 1492 bytes cms/static/img/large-freeform-icon.png | Bin 0 -> 1193 bytes cms/static/img/large-problem-icon.png | Bin 0 -> 1522 bytes cms/static/img/large-slide-icon.png | Bin 0 -> 1569 bytes cms/static/img/large-textbook-icon.png | Bin 0 -> 1797 bytes cms/static/img/large-video-icon.png | Bin 0 -> 994 bytes cms/static/img/list-icon.png | Bin 0 -> 960 bytes cms/static/img/plus-icon.png | Bin 0 -> 952 bytes cms/static/img/search-icon.png | Bin 0 -> 1196 bytes cms/static/img/sequence-icon.png | Bin 0 -> 963 bytes cms/static/img/slides-icon.png | Bin 0 -> 1992 bytes cms/static/img/textbook-icon.png | Bin 0 -> 2445 bytes cms/static/img/video-icon.png | Bin 0 -> 983 bytes cms/static/img/video-module.png | Bin 0 -> 117780 bytes cms/static/sass/_base.scss | 1089 +++++++++++++++++++--- cms/static/sass/_reset.scss | 57 ++ cms/static/sass/base-style.scss | 1 + cms/templates/overview.html | 1002 ++++++++++++++++++++ cms/templates/widgets/header.html | 8 - 29 files changed, 2048 insertions(+), 127 deletions(-) create mode 100644 cms/static/img/breadcrumb-arrow.png create mode 100644 cms/static/img/date-circle.png create mode 100644 cms/static/img/delete-icon-white.png create mode 100644 cms/static/img/delete-icon.png create mode 100644 cms/static/img/discussion-module.png create mode 100644 cms/static/img/drag-handles.png create mode 100644 cms/static/img/edit-icon-white.png create mode 100644 cms/static/img/edit-icon.png create mode 100644 cms/static/img/expand-collapse-icons.png create mode 100644 cms/static/img/large-discussion-icon.png create mode 100644 cms/static/img/large-freeform-icon.png create mode 100644 cms/static/img/large-problem-icon.png create mode 100644 cms/static/img/large-slide-icon.png create mode 100644 cms/static/img/large-textbook-icon.png create mode 100644 cms/static/img/large-video-icon.png create mode 100644 cms/static/img/list-icon.png create mode 100644 cms/static/img/plus-icon.png create mode 100644 cms/static/img/search-icon.png create mode 100644 cms/static/img/sequence-icon.png create mode 100644 cms/static/img/slides-icon.png create mode 100644 cms/static/img/textbook-icon.png create mode 100644 cms/static/img/video-icon.png create mode 100644 cms/static/img/video-module.png create mode 100644 cms/static/sass/_reset.scss create mode 100644 cms/templates/overview.html diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 3223bfda29..e259a465c3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -102,27 +102,15 @@ def course_index(request, org, course, name): if not has_access(request.user, location): raise Http404 # TODO (vshnayder): better error - # TODO (cpennington): These need to be read in from the active user - _course = modulestore().get_item(location) - weeks = _course.get_children() - - #upload_asset_callback_url = "/{org}/{course}/course/{name}/upload_asset".format( - # org = org, - # course = course, - # name = name - # ) - upload_asset_callback_url = reverse('upload_asset', kwargs = { 'org' : org, 'course' : course, 'coursename' : name }) - logging.debug(upload_asset_callback_url) - return render_to_response('course_index.html', { - 'weeks': weeks, - 'upload_asset_callback_url': upload_asset_callback_url - }) + return render_to_response('overview.html', { + 'upload_asset_callback_url': upload_asset_callback_url + }) @login_required diff --git a/cms/static/img/breadcrumb-arrow.png b/cms/static/img/breadcrumb-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..5dca714363fe95ef788b554d55e9096d8c9fcc9c GIT binary patch literal 1700 zcmeAS@N?(olHy`uVBq!ia0vp^fr5%(GQ`zk9!uLS~AsQn;zFfp39xYDT62(s|s5su(?)1Hb_`sNdc^+B->UA;;0DU00rm#qErP_J!9Qu14BavGc!Fy z6H_xYLmdSp14AQy10XWfH8im@HM24@SAYT~plwAdX;wilZcw{`JX@uVl9B=|ef{$C za=mh6z5JqdeM3u2OOP2xM!G;1y2X`wC5aWfdBw^w6I@b@lZ!G7N;32F6hI~>Cgqow z*eU^C3h_d20o>TUVrVb{15Cdnu|VHY&j92lm_lD){7Q3k;i`*Ef>IIg#cFVINM%8) zeo$(0erZuMFyhjbK~@!5ITxiSmgEh$kf!)*v-k@%+S!)(8-2&UI1Ke;qFHLnDwHwB^B1gBn5V#qB3+U$~Alv$RV;#QQOs{r=2RVEgV0J zx*54S1I;tV?iPsN6x?nx!s!-$pkwqwQHvDSFd<<20WskT7s!Dp{nR{QdM^Sd>_irG zM__&t^K@|xskk*~##!$vfg;D=_gv0cYA7b<9M7>$ASNWCJ4!@EFN#IfEF_@g>Xhzn zYkw(cB?`2xJ`^S57$D-PcT2>DMfBy88CFl-&-EBbMg9CHQ+02L&*zWxsvkV>Ia6@| z`MmSh>Q5@;rW>X${+T`hfb!xk95?v-8mu;O*|5#a64l6L<5pl-JLs5T((;7=jaUYw z;s;ilt6GBJnlvXYU!|+(b>!ZMQcfGj@Fp(K==Z;W)C<3044m$o*L=1os8NtR%o#vbqRbPIvZf$byZsu*D{-#g3pk-K7b&IPq=5fURdCfebdzhb_ z^qIPT-NGbwurh5^mUWw@(Rs6pNqj41vRB`96s;_9SbbzeT~V0r)}0!H(k$6CI@TXh zT`Q!>*e3DGGC9 zM=TZH4{ELzI{eZi@oMnqV;jp}PidLW@cv+AZ}R`00x|6N2dyTsm`2U-IC?-rSxxuL zrm)WYcDDPv7FkVq^=l~nFgIGN!$RlM>6Xbys=QAML*WQuF`EiF49w}U*|mTC2JAtzl8 z&Q4x>#V72xf0U)!lKvXz-Cg^IT|6fV8=R`DjC~amcQULw&nmd&a0spF6s5^-veiwq0NR*YRD$Qkhw4c|4a_t}1?G*0%XT*In5q zr@E(loUias_cE?qx!1(?=H5p&_n#MiwmN4RaJZGtWP0}Pg+XgAKQuiIjm<1Kvi^ML zO4!+n%1K-I_uuhq+w5Apv+v`}q86#dV6!OI^dC>Q&e7zLt=oEHQ*!Ifh1$1V|8W>F YeA1u$*HvO!0;t~bboFyt=akR{0I2na00000 literal 0 HcmV?d00001 diff --git a/cms/static/img/date-circle.png b/cms/static/img/date-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..10b654735d6319fd0d6d4d40f3eac081bf82690e GIT binary patch literal 1653 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#CvFmpC@ za&$9+>2=9ZF3nBND}m`vLFhHcsTY(OatnYqyQCInmZhe+73JqDfIV%MiQ6p(IL(9V zO~LIJL!5f`fsWA!MJ-ZP!-Rn82gHOYTp$OY^i%VI>AeV;u%}*I%*w#P^xV_MF{I+w znVGx&MFK^Ro%frj8<82K5fHGL&BRPiZ{bBIDS5>X5vlpDYCDvoZb$t1A#C85yr#y? zJ1J?K)1sx_7mu;4cqfTyPT|@RA@s%i7w^uSXI}8^-dybe>FGV|-Nong-`&|+e14|O zz7Jm468NsLeszdk!F-Ej>CfLnCfvTxlP|D@w1ge9@@iqpY1VU6U-)gpzI_h-3!^Vw zH&7NlV)SMQOYF*q=(f6_bJFGVpYK%tHg%=wgTNQUCOk`R>hdpGKP+#z>05eZ`nSq8 zQU_E@q^r10z2Ex&u5pq-xuK!=g2a@`FLd4GdmS0SX306%2HC$}d_G0sK;Vk_SNVUx z{PlO3yXCp6UGV+me;JJ9!UdW&ox8;!ob8x*ZNoOXU4fiCIIYyb-H0?=%M|yZDZATr z!b|=Iucq8cKlNnC?n!3cj8j*7GgcpsdmVVdHs{QTr+Lj@DW|j&)TQNhq;~e}Sc`6$ z+IwD%@!W=-&;x2_VmeGQNjIN#Jbfmiy#1V#UU~kG1r5bHIU6|&q)%J2&PhsM?%H4- z;ivgPy7^X{dDpS)w6@VV1jdI>aqz z*}m@^*ZF9_vsy8+Zsyn5hD>qEk!qJ;wS~5So@dC=uRGzf^&7sPpM`vm8JX~e&@8ixf zeOZ6rw)al`|E5P?(5^W4t1e?sce)7oCyQE++BmLBzch?@=}Rd86q8z^c!%M4iTgXt z+G))UtNym|NpIiXD!IaG>g$wgwLOx>ezh_S{W-Z-ESq^*X8jxXe%T58cRrhbL16wq z1^*WU=QTI+C(c)Yk$m(0lLeNq(v~TkJo2y1Ie*_cCBo#}wAl+|FNF1LE_rJfCRN=r z8f2&TiM1s8SNscS$#xEx+qj9kcw~i;DLAMDLDjQ*umLS+eET!ZF-#)uw%WLD-qdX5 zaTbl?VlN9~5 zfn2N+0uN#f#NM7CX|c}Ecr{{EH_w7u2p;I{K~Zh94oWD5K<2hGwjhc?Q8;m1Ruu6H zkOWcU1+tWkxUI>GCS}3=V@bBq>1qw5GS5X$I@`xM(0G0{8gZj74uw5lR8^I7NK%GG zWTLT;tvKUH8w&;lMt0~1*hM~|j8+E?vCfi84_feoC9NOLuZdh3A6o%0asq8>5ontK z5B0odbc7r5u-<U zWDh;);|10^#FkcKsTjO~WHw;v-hoafL>`zmt+}gfQCITKvCisR3M;WZDTb$`{cF?< z7ts$jm%=57rSzds^d1sne;qw}OCLqmD7515o!`@GK5^z(V*gmO^?uEM#q8fr{$RLI zAFro(i^|1I&(fzKJpC}e`YpIWj8DEedWt!cWS+l1p8E1=^D%v0)pE^vS=_z%2Ns4a A>i_@% literal 0 HcmV?d00001 diff --git a/cms/static/img/delete-icon.png b/cms/static/img/delete-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1855a2943dc4691222216a3a2a5b79ebc51a6c00 GIT binary patch literal 970 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 DGqJbl literal 0 HcmV?d00001 diff --git a/cms/static/img/drag-handles.png b/cms/static/img/drag-handles.png new file mode 100644 index 0000000000000000000000000000000000000000..391a64dbe06d2bc42de172a0f9d9f1b000467547 GIT binary patch literal 954 zcmaJ=O^ee&7>*Ryb=f_LBDx0|vK|z+Nt3o|J2tLu)3}jMDQ&@e+ccTBp_@#cOl{hW zvV!2h5D$6~#JdZ6)nA|I(a>e}4LdEmvr@O`Et&M;1X`$-x~24A1JJ2C|&-gO8}lafxegt4-VH9<;G1 zSTRP3ynwMeuDBBgmOVfebWqRrRsQF@_dIYNmA{uYQ)W;>eRp?4P;*ji*^_}?aQK~D zpcp|W;2~;($UE>u7^(bJ7qWe97I`p*(1FSy2h}#~pn?ekSs|UYQ<4M}MUXOCMUl3F zoRVZQ#a<;TWnfl;at_QMo_Qlq7dA9~=8K(FzE5cYMR7D538S=tNl%mtg+i<$%Snbv zhGU;vQPK~$<_a1LZQ=&h#Xg7?tqvYim1mJ2r{Dz(SwEbWi4{zYtU#26RGiWrXqx|r zdfoyZ(gr$-_f+AqH4c#2Kp`Fyn>DVx6}t*xg&>P!(!%&)Ud4JJQylhj04hxdTsJM- z^<%@0DZ@0O;fK`nZDeRF&ol(rbzoW6^SYid=%q5NdsQmu z&_h0*bDa}zX(=}z1}|WqHALKp$k7S*z%*&-E{`R*RBy(0mdBD`$`x5KV%*w)T0J+h z`H8oS!DWZV=p&!aJ7I$zxkq=|rPwyKQY-q}DUv^*9<8kIJGZ&FmoFr)emH#j5vwmQzIg~1(P_!K5{2w6Nul}aUF zsn2KH6BHc`23-w?@nVG6nKqDA_8Lx4RY8D`Vrv%COar)zl57@Hn8cB8r=VLkS;N^W z6D}B4mMn_)`Q4PNKvDcZRM%^02c_X|y#Ew-GSe2M($F!Bwt^cs-s8Hm__z%vWZD_i zEL5wQ%9+S?a;631qZ~LcN{VKosIh6%>~EE`~|T)|X|Iy6v~tL}17)#bXwpj+6p0Bvmws)M$vgRP`_t$r+Q zUA-NyT0a)9E|! zBTWs<`&Pe5M~*e#{5-qPb#$*JqTJWzP_$BscwX<3d!Fa5<5M1a61-^q)}9jE=kB#V zkVnAA!PT+b{o!W0yTGkCu6@{SvD%(499n9=I0^2{9qX;DcivrJ>3kHe>|d07QSbbX zA+RyO)cJjHpwO@~n^+rp^0v#;qGP`ndyQ28Y^CGs2;I`;=_Ee>xI1_5^sUG4^-U&* Kg=YhoulxZ>fmVS4 literal 0 HcmV?d00001 diff --git a/cms/static/img/edit-icon.png b/cms/static/img/edit-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2da9551010a8cf0c5feaf41e89817f79b593bd9b GIT binary patch literal 1066 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 literal 0 HcmV?d00001 diff --git a/cms/static/img/large-freeform-icon.png b/cms/static/img/large-freeform-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b1d195a7ca01e951981ce49a0c85e719e642f026 GIT binary patch literal 1193 zcmeAS@N?(olHy`uVBq!ia0vp^N8U}fi7AzZCsS=07?@QuLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}+T7#d8#0MoBXEYLU9GXQxBrqI_HztY@Xxa#7Ppj3o=u^L<)Qdy9y zACy|0Us{w5jJPyqkW~d%&PAz-CHX}m`T04pPz=b(FUc>?$S+WE4mMNJ@J&q4%mWE% zf_3=%T6yLbmn7yTr+T{BDgn*V%gju%axyeCGIcX_adUEUH8gZJbh31Db2K+JGO~0w zFf}zdg6Vb1PcF?(%`1WFO+n~&#iP^Az7H6D#^?{Dj2SqJXRKtXT=?BDwCtM&0p7c}mfa$#mn6QP?&H%FoqqL`sV@SoV zHKB}xhZT6I4dtL zowCb5(NF$1d<*aP(aX2xb@q3d@@1kq6lJ`Z7#6ur@KE15Lu6EaAQ{;)JQL1 zSO4r+$!p;dJo$Y<0Q1!MULm2kmWTXV_*C}cqOJ~&Z7ZFYME%kAJ-aw#g@}8a?-_sR z(*LJVF1x$!PCZ{W!@B)tXY+ThcU3W&6nfLUfsw&r={LSzi}aj9g@>oBpUXO@geCwE Cd6y9Y literal 0 HcmV?d00001 diff --git a/cms/static/img/large-problem-icon.png b/cms/static/img/large-problem-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b962d42b148073a89b56f5535548aef6d5ff00ca GIT binary patch literal 1522 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#MtG;}p|vUG8CG&eLdvUD~u zH8nSa>2=9ZF3nBND}m`vLFhHYsTY(OatnYqyQCInmZhe+73JqDfIV%MiPJ5HZaB?@ z>P^Az76Y7m^?{Dj2SqJXRKtXT=?BDwCtM&0p7c}mfa$#mn6T$7Wo%?%U`p|HaSW-r zwPyNx?`TJXU_E?2O!Y#r*R4NsatEztg_`t~+z?TsN!Z1Irr{5iE8m znlvXY%a$$RjC#OQ!&H7vbn1pil^aa;Oy$A*L?;-n%@sP^eE;BsWl7e>Rn$uzWIVd9ay=d`@UWbs+eQR%Dytz(4?8edgZLUTOcy2xNj9QS;@%v!S{fvi( z8~qOpt$eki)lNdlFIv23^-0x@sfXTG-75V5%<0j7!}(7wx~?;6r}#Qg{LT6*L0-hM zr$YOWk>bbdoHyz#zGYc&4!MzZf_LW)P3d&oK(Wh$#%AvKdv2F>+CI6I;^X345q;)~u$d~WLPFFpCjYKl)V z>+B6jew^99+$(L4)aLJR^0Mx|$ZhYPdQI0}tWxpjHXBFNL-(d0Pl_^Qs{HuN%&kCd zeYffS_N{il4=sG0H@v;|tK!N8*>9QO{%|zyv-mMp_U0_x{-}#?^Uv6_t4|I;7&zY(yT{YEXb@Lz3iYblj50b$ lYj6C4^n&E*59=A28Q#q+&t7+SQ5&cf_H^}gS?83{1OTJ-KurJu literal 0 HcmV?d00001 diff --git a/cms/static/img/large-slide-icon.png b/cms/static/img/large-slide-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..04241fa2f7c518f8cbef503afff6722c56741a69 GIT binary patch literal 1569 zcmeAS@N?(olHy`uVBq!ia0vp^N|6H_V+Po~;1FfglRhD4M^`1)8S=jZArg4F0$^BXQ!4Z zB&DWj=GiK}-@RW+Av48RDcsc8z_-9TH6zobswg$M$}c3jDm&RSMakYy!KT6rXh3di zNuokUZcbjYRfVk**jy_h8zii+qySb@l5ML5aa4qFfP!;=QL2Kep0RGSfuW&-nVFuU ziK&^Hp^k!)fuWJU0T7w#8k$&{npqi{D?ot~(6*wKG^-#NH>h1eo~=?wNlAf~zJ7Um zxn8-kUVc%!zM-Y1CCCgTBVC{h-Qvo;lEez#ykcdT2`;I{$wiq3C7Jno3Lp~`lk!VT zY?Xj6g?J&i0B&qvF*KNf0j6J(SfFpHX8`gNOrftYexnUy@&(kzb(T9Bihb;hUJ8nFkWk z1ncniwerj>E=kNwPW5!LRRWr!mzkMjW$fl;ZscTS;pXJxVrb}U=w#{Q=4ftcWMt`V zU}|b^1k>x1pIn-onpXnTn}X15iBm5qG2|8iZFWg5$}CGwaVyHtRRDY1DigO`%y60q z)tiFbE#^4&>H{644~kl(sD=pv(+`LVPq;u1Jn5(A0n>XCFkv4n+j)E-5W*bd~ya&|t$JKb=KtkDD()5Vl}`*TlEs zVSxWW>COE=ehS=p^8NOk=?A1T9^TM2_?gZo;S_S4<@nFPY2WH^o%^*fRxWKnmzzW1 zswIaQW4mUY)D&Cyfw!dD-A;aD;F9zCd^W2$pWo8-+eo2?)A#+O2wrX##`vtX`*K}U zvlDN>S>N>fLA%6q;b~UeG7aKVEwr~v*S6g_6sEpBu;AL}6QUnv?l2sFu-L%jX;ki= zGesT~j|k2Rl8onAT{e;Hsm$Xki}#sbUGFyWMu6HCu06rqbUv@R^QrBWm^0UocZ)CF zxG`hOhhZrZY{9}IVE>szDt>6F;B&gV)# zStc&>XDdV#@0<)e9LXlKb8^Q@3nM23t~tB2t{>3-(0c5pkkiUS7Mmx!Z_X-fJ>|Q! znVrYAC^YSA^@FDLJ2s-+a%Y9xpRDq;d>Z*c_J^OVUU1ds1wqAo8Pr`Stu}Cpb(&sb z^Lox2XB+vX!es9pE&DxBAH6?q+O~UZ$gw;3tByS3d)<0`p5boQ^S6#i^{Y6o3%RrF zxkKC)O0Y|en&4ZIrK1Sq9pf{dH;_j74Kv7=6E&ttWnjI(_fv! lE0ec9`uRYAdDebb35LYiQzy6@$Vh++ZBJJ}mvv4FO#r#%S;GJT literal 0 HcmV?d00001 diff --git a/cms/static/img/large-textbook-icon.png b/cms/static/img/large-textbook-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1ac2db86d2e6e3336d95c5738ec9b8e716a743e7 GIT binary patch literal 1797 zcmaJ?do)ye93O6|b@dZNHni1zUYywns#KnlWqKrKy@DJg0y`hzY7+3U?XH1GieCsNTL zgW|D+0Y69q0wk=Ps{l_R0Aw6vc9kjZXB8NrQlOB? zVF@Gy^osmACHH$q!dGPz%P^xII;NxiA-;B z{=i_fDJ+>1=F0>ii$O&r8d!-$NO23Gdl2bFzW_ReK_K`O=$?!Kng@YM^z_4%NFKyl zE&~#zNI@Aq%N2g&GUny#he0YwJTpLrL-bOZ9eq_P;aF!lX{!sx)T5R3NFotOAL z?Mhzp@in~xG)tDn+XC|h%S${U3Nx?|!=O%ZE$J~479kM>*$e&v?Iu_b#~cfIy%G-$f3QFNosQ#p98xZ%a39Jp6mIN~&gao&d zpsx0qL*M93@XMOB@__^6WJjLy8I5gMK$&~)lrJeIXiwE}K{jsU&^m80!zyz_uzAxm zdk=;0=3$4j(voYPTkM|w_dDKeDY%0z9IiQsZJKIL ztG%3IdopxwYr=`{U;4>+lFhXbpC7%|=;Zyo$+P}RMoXFRkt$ITdj8X$k*p(^#g}2t+od5QxNldFj3aE`(1QL7QL)iZ|m;~ z`s;9ivc(PCr23jPV71!%?sj>;!wH8#)mZ(2kDai3;_>;VJ{wF%JBp$?s@xxmrgf7& zgGs$r-wzAhRDugfCn#y?IxkTup>il+Q~tF5d=33&_fD+ScyV8D6vM7|*xhAwH1}}x z@>&h!2ht8s-6U;gW$bTjPCdBu`j%C7TUPGS9-XM!ur~00XXcl&F1y>mzP)&~#k$$j zp=zY^7%k&KTQzNeLsO4yVcX|ZP}l2+E1sHbSdDA$8{6CfsrL7-Y})|A#K7a1XXX}>Diq_5I#kTaqf(c>W1RYFFWA{fYHN-E*@Qga z!Fclyb?w=!E8Zo4mIiIoeSwd(i9DZIxb|j;-!2hTk{V?*Z5fNYm_6ZL#i14He@rZY K4&w|xHtQd^OThgA literal 0 HcmV?d00001 diff --git a/cms/static/img/large-video-icon.png b/cms/static/img/large-video-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..392851324cfeb9daf1d4efbaff488cfb4545f3ae GIT binary patch literal 994 zcmaJ=PiWIn7!Nqc9Q)(JgUY~X=0Rai^3o<I(!_wq!4O{YJ@@BKS zivtl4idR9sc<|@W~P2VV53mtA)ncyb`jm#*zRbcQ7Fy^rtr`+nb__iS$V-tgd! zL55+5%O$-+*D?BC8|bJ1qt|bC=yIJDYGfX-kfs?RCTrt50%g}+MHOV)n-309hG8z< zcB(Z}GiFo^yPO&OaFOd%G{a=3Bj2>v5CL_x>UbJ^ytmH+$JW>d$q)=bj~Y&CD?syG zvsG(r%~EW3dJ1GBl@hp!m>_c3y-d4axi0;W_+R>d@EKP-(F*ehy9FScXRlg2g(@l~F0Hk({C$>Cs? zhl--a9HN+@9*J<%BW9HF!toA+jzTMNeBxja#EfPgZxD^8O3zYo{VuH+w#!5d#z&^l zLr#cO>Npz4|6N_T>m8B`I@jBiIIM2^h_9d!Zv+-?+{$gLGFcF9{Bd9gikWtmwTyed5Ue0V>;b>+gW{vo%#^A~U+I@JIG literal 0 HcmV?d00001 diff --git a/cms/static/img/list-icon.png b/cms/static/img/list-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ab46179cece67b2a70d29579363b327d47422715 GIT binary patch literal 960 zcmaJ=&x_MQ6i!jgT3GLjhcPRnVDqa@+J?r`HjP^_rM3lo@L-xu+t5uWCR3BvgCc@> z5-3ku@Rg9neIC-Uur8!{trK)cVsz@YhBz%eH~7aLL94R)3cDhy2D_OOjy5HlJ*G{y=|lpdzwcq>{rSd@trjExMBVZj9E@#Saz)VcnwA%}a)s2r##eGeCZ8)*IH@XC%35}b zRgpP%po^DS>j*1t#KyzmcqFn4eftiyG#@!&p0sSQ(IVyqUgE@>Rxa1pYdOSPo3&&& zYGH{OHg4@dtzMeQ{KVVU;F7~?^r1`U-6w;6_rsTcaw&wmTJA)jdpBSI`FJl0wtl~Y z#}9slXOq89CN3m}#Mjwu_Wi}v6W{i)ms$r;s6>)-o}as)dh{&5qg1`tP+wMVJ@^Ye C1T35Y literal 0 HcmV?d00001 diff --git a/cms/static/img/plus-icon.png b/cms/static/img/plus-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3ffa4f2f69b122c9b27891e1c2cc07efb1dbc42c GIT binary patch literal 952 zcmaJ=J#W)M7`CWDRX-NCGM$_XQPkKzCw5{Jw`;u6xea5~JCnF0& zDg(a)AyxbV23C;Rx>WrE2&77kjBrkql!0K`zPne?^Sqz;x>0|!y>V}YHKve~pRa6!316hKy zD6v;f!>q2Tx|{>^hiBeGr>8f~>f9GQ8T^3KKo`aFcr1)F0tx#f)HE&8kmWQ(q@#&X zQJnUp)I!0;ksZ1Lb%_rWMbsrDYVa)5>lD0TDeFh`GO>b*F$zQ|NJ&Zypk@6Z>Um3a zM4R{~-hT>5?MZ;eCXUD`v{~bNsl-*FmqUyw3EPC6EUMTT5K5u}2|&4}f(I6|T|Y7G z%@~%Y*Zhbg-^Ml5;F*Ttx{j`yvXYnbnQBSNLs)?&&8!x4P?oi_q$oLg!8M6J@~}@A zT<3{LVIfmzbJyE+!QQg6<6R>vZ*b(c7=S{kS?$?ozE%H$Rk~z5E0HDJE+G literal 0 HcmV?d00001 diff --git a/cms/static/img/search-icon.png b/cms/static/img/search-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7368f803d586d41a91a2842498e6999f9372b3d7 GIT binary patch literal 1196 zcmeAS@N?(olHy`uVBq!ia0vp^{2c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#2=9ZF3nBND}m`vLFl!>sTY(OatnYqyQCInmZhe+73JqDfIV%MiQ6rvIL(9V zO~LIJGn{($fsWA!MJ-ZP!-Rn82gHOYTp$OY^i%VI>AeV;u%GWdV#mP1DCgFehj2RMRa=6tJcu{z%Ny*0NYwRV|*!P#HtB?-rsxuvBorfpoj z?<}Kiw@r%y+=MbOij6;ZJHYLjk4V`-b#4VY5e;`Si2X)7 zCRs0%@4|YT{C_?>-Y3gtTfx~&K&oV;E(E%3_F)s6_T=ajEKpSHn$zy!j&WD9 zkjt1chKXFCuqmogjC|7?LJYdF?|3Tx>)i($IJQdfiw0--6*zGArU7hC>uqZ~v}Btu z-Ufw8Ap$PMCWzcaFH|Cxp6e=PADdYk%prKF(x*Xnj0UKn00NQ8W-N~9fg~|}PLw46 z77#dIU^((i89t|ok|N~6;-QJR!0stct+wz*PAWaX*jHF~JRURSEQ5kR%geGHYY0My zATr^^!)BE6!kt7xgP|2TK6a1?VnwryMp&guq^Bvk{!-Qp7iA&^V+Qs@e5DdV6QiJw&|y-=#sFay4v-HjEeYH( zOv~|N!_7IvFcjSjvFTY**HoHlFpgs@VqV}SPTZ}PN>!e(^5wjc-ObBoPO3=dQZ1Ko zHDrxk=;4HGpK;}t+;|vVpLo__;2cA{79bbQlUAJ7u}CZR7F>IEEcumOmITAbtv%Q3 z#6;#N-Yy519G0UGJu>eB8SIyj?mZxvLe#ZVJNnVR|MvHnlXN;=OMm@%a>?HM_WVzj uEV1iYPDu literal 0 HcmV?d00001 diff --git a/cms/static/img/slides-icon.png b/cms/static/img/slides-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1dae5185be9bfcf19ddea4bfe94c632d822cb4d GIT binary patch literal 1992 zcmaJ?e^3)=8jct&N-P0;kaAFB#0qMDVglJCDj^9ZB|-y6bUeSqvtN&cI#{mAL_6a{yK# z`OQPYqG^?hfJ`8I6AL@>D9I?Q0qAtSUQg2p&=74Boyp~L?Hnu?74x9#GSw((psIDN z<{1Q#PN7w4P!*yk+Zn+GBpu~ZuuOj|0oJ^wRqNig32PYL0BYz=8pB@FyrWe5|6O7D zt+x)9LBHqwKZ$iQnHq>LgLFu`R)GyJag{xk1_;$cAc|;X5F~BBi&4o4is+IN4LLNL zL-vz`3YFS!@PCbvN&&H2hk|MaBo^=}7za(IQUZYiEVhUx3K8)G*-WO8$>$10A%RR5 ziyO*dvjbW4SOKC)haojOk5>sS5S_CG)E*elRoJ-Jxd~dMI zg>&I9h^1r6(CwrB!>H#?n11Zrx5C8^Z>0~ZF}-UsVdn>}mEdrAKr9G}G4xsG+oS&+ zM*J#u+YXhZn>%0Rd!Oa+1g)+%*JV!Zid>6tFNvQF#d8|cS9*PVqgh}0;-g~9AJs0h<_}05^tSK0!w%rc$GOZ??*)8K=QCKB_vS1K?U~##8%ezB z8z&+0_GV6l!7O7{MUuwZQ9B;FDeL*mABbK(tiJ2F=TZ(5YoFY?W67E`51mat{-aB2Pza+76Eom%!s+`%+?B z8vEmq&sl#gV01Wlf^FADl{;!#1s^(D9OyLqzNxXz0cGO%|HGL@TiB>o@d=ehjS#y~UO^ZX@Wt~=3trO>Qigo`BX&t})Z`xq}L47wB^ ziF{GD(#!kb5}|9$zBwBo6awu5dGJ4O;+;byB*`wh1n(k#>&`Yp*38!7f)n0d6xA!= zW!eXwN{MS}(CSB?pWI(ry=&{utUWy|lIk3qj+Qx|s}jV!uKO3h<4LeK+IvlNTybei zSNqV=oz~$^^0rH8)gsbn{E-i4;`(cf){%(k&D00@k4#&|M z{Ssg5Nn5gem+2PDV{ZOPoMfpU@j7rhvw!+OkBH%eiR|RXf4!AxyZ3l)#VKv)S;u<9 z;@Q9aI>Ws0I8@6-MLCtIO* z;KZ|&YwCi(DF`PjhngL~UgbO63;^EIaF5);Rfk_?MdHcf&}Je=hkx?cUTC?%#jDuKA+iaeC#2 zgeFrGo)JEbT&OCV5T#5VANhn|;a1GQ<~;FeaPo}hr}pzLjwGCqLKfv2wqn+Pw~B>Z K1mE&^=l&NIBrc8s literal 0 HcmV?d00001 diff --git a/cms/static/img/textbook-icon.png b/cms/static/img/textbook-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..11e4abb363de7d19118fdb76633cd117cd4cc4e6 GIT binary patch literal 2445 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⪻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

*)8B5Pp zm7R}|jHJ^>@iJjpGes(bB0?Dm8f$8GHXC**WH9o*4C)&bJZ3D=3LD+nBP79b*pLW^ zhbBnL2cCqh*4{~wAmKKrcUM9JZ;#aiIyDsm|A?^B58W3rA{BYikYj`cd6(w3w>rOb z?kv2flYG~Y;onI5859A)+oq6u` z=@^}R*xmQ^8#WY-O9o1X8L$e7NqnP{0x^-8U;zfZ&>l53a%3Vy65M&C5`ZchDM*rH z5hD5fORKTDy8xq+Cx2vZz1q+=uwOqA<_~B>#;4w~K zOPO=kCExy7wQ)!IT@!{s@BkO#&-bAPa8|@Yo^X%>Hp!OmI`5}n4e3bT!HqW~>y zg^U%loQxDzj8>jtvVd(hm{=RFrg4!Rij0YkeC6eoX1)GGMfJV|x!dt6vFP21R;ln_>0wQC0JI)vpt9d_lb^J z|bvuJpL$1=xf!|X1NTQO{b0=G1t{$iU>)8wh0zOoWQQ|j-_}U4I)Z-U$Wsy8MLq; zYCV3!Qs3z2;Q=&is8Mh-qO7tEv>Es|K*4n6Z1E|Ugq}X$*g6{ume+0?5bCcUkre&J zf;;Y>;o59yDJ%huO<<_lPqOHXSd6A;pK;i3gd$m?DNa+5Cu7*FGSB&hka}Jo9%@=q zeZB&;8c5+NDNixa6`s_RG7n#TxDcAnHkKpga>hl|zLBO-(2yhzhBjP?Kp7=SxXS0s z?eVNPtbJ+WqE$uLg(!4K@0W~gUc5f_UT@eN)>E4ed)F?{IM(>96SCgqe3b=S8crU` z-JHQA>*NKaq`m8z)rryA9YFxlMOZNE!j8Ia-FAr@-#u7rm(yb1m4(-(NBGD;Bt+Xx zyenp}jBo@b=+r#WQ;&cY!Sm9u9fH`bwwlac#e4U8)iw5;bH6MmLWUi+aNtZ?#o_!j z->j{-nC^UR0p;fkfW;i3Lt6Fmg2K4`BES7FXfzba+Cyy0;vz!S$%zPYXMgyy^~C7`qep@8P)Z50v1)6r zmF8NJ3DY~CFET0yA(pStWJ4@~Bow2T$#`Z)QD&RqJC7$|ggw$234xq%9?JlHegJ@& zu%=)SxIdr${A+UV-4p?|}WpbH}2157`zC71H-ZDtG z)#*Vq0^UG!j3VJ$w0E}!3q>*j8RmKwr!U%;-OxWh)vx=dmf~$$^^QxtbmZ5w-s`!N z%TRm6I(5f-?&kvqR(9Cbc=unul`TcvvU-v8mGb*lufBPhu`kg%IhhH6c=`Up?zi3T zcEcak2-;(Dig%6ur`{Dt@bVO2-5DF|DKETwH6iM;f$iQ?#7;rHHH(AhIw$W!>|jrB z=`K~*Ew5g?IZ&yFj~#Qipy0<18+PU&QTK}^HHucFp{Dd)Q`v=RrR)8VJQ9B2eZ2HJ zh6#)a4~vf4wPokdM;EAc+Vhq+SzQy=+=_hMfYsVQ{c0|hf42B3q{sni1nv$~yqnE)vcIFVbqZKj*>bvsl~Eymqkuxi*GMZB zNG@+>Z9?EVqT@w^Wo$MZix;JiL!u;4`xFX;&0^)rr*QfZ1o)4ha_8P-?|=6BXH3p! zilWNSmnD*#)R}jK$Oyo(GFlGga&w!JWf;=&y3)tnhoI$+`etBg1Hi+|aTFn)y_4`O zXz#kKWW-;(>kEW?6ZYN=cv0;RzGUsTI9?=6Z`FyvAN_fAuB%^_-5(AF296a)W&dhCZvpnW+ixvT$?jr5Ho)TX#?VU5#LkMsC;WOtD!mbT=kbfWp-FTjaOR`t26{BUYkKR2h z*j2ybm#UIdV#o0|#L7q*>+T*heA0llk;Gl2fv3*Qr=@7h&nNl8*?NOTb@Gh4p)TId z#Wy^@x~957!3uVx3`AClm=KLiNMO)8n}y>zpZ?L|cZ>mdOvLemd9viD4Si-_N}y+D zMowE@BYEz$3K22hKEZQ`MUEc>hQt7iP2=e)*SZ2ZjrV2+Q(9JxCo43X;W(O<4T$dV zX)=%u>**QlG9)&9+EnbU1QRbZZ0`MoR5If?KQ~lWX&K-V8aZ*sbeDUk0vB2a=xO)d zmdhdOfsKqAI7IQqV+VCr~QDI)+& z_Hok=h%GNV#S_W)gBvQ1i!QD|BU9qVnFBJ4kd)67ekE8AZ(6Z_$0?JrczJ{kefZJo z-V|sl-SO@ByUx_>`CpZRN%Q948$_0T`1j9MW1e|*e6X0aKJS}>x)wj{6tsAPJHO0@!^h4%~>m#Wgf1^9~D7U=gl7#szR2MPhb15 z_uR$l{Zyj*C*$2Er4f(*X<9(e($_O82~hdfi!TD;IqvTZCn@aDI7`ld_)8F9dWnB; z8u!{?NAM36TgCn_-uFcwWf1Y`~(caX@?hR3Nd1e}aeCuC^ zF*DTr*W{F&>G(NsJQ&wKS1>J#@oT#n5#$Yv`8@k8V5OfKSQ<4w*Uc zzGSsS=eKnEmU^rKq;xv9Rj+p)`NUtRg*y-KkxeVsISuYZ5A|QOB@wm{qLD zgPS(zkFkdAFMl{_5(YH=@yC<>W#0Yb9811uOc;Iegq67s@0J^*z3Why;i**H$kkRc zh+{Mw+N%#xYp!y=2R)IzOR~34RPQ=1OC7}su9<79&1cV^4Gj(T@>U6vPI+QSA`V+d zBIHQSul)=-UEI*z+#C}V?&5Y?uMs`#_&I^H`@8Efl3?4boQdbY#Onkv|Z!ZuOh+NcAK>#^5e9r9H z5hFl=H$Di5UlO6<>lz8h3sA;Coi(cDrxpMocgA9&6l$E(!_z)wd{V9J zr3w~l{r#wZ(LAjbqPdV>!&9b*hB8GZO%08djg@ItngBl#7KDTK35GjbSMBJhpTJGU zZ6-SAg16_W#~*Ryr<~~3*>MomB`RX}U;Yd@5~yiG%?t^xd(fYs2%I#=a=ILmKqNV#m}6Z4vC>qviJ%;b2c$Z|?rnf&BqCYA-k?!e*rPJ)D+4{K@0${nY| zlBbP`)-;~YKUl8R2rF3QspD0$psBM5xwPf4{qex3--f>RSX^u$l$o`oe*9cdfvoJ? zaug_s2B|w0*mSC}iVB+ghe0j|d&Yk}mWXvyGSqM+mx&%d=bqrQ9qV(pEM2C0eqKr+ zo|9$1`+8O-j2$y;XrOiXhIJXAEI|w39Hli^GK`C@g98}#Y)z|~UtxoXrTLX^sdOKd zK0XA{A#}&{oC+Q~s>>z*wUMblULKk{cV5JW0~y(S*L>;j|74mkUvKsYZ)KLt5+>ds z?|Odynw(GGmi=w<3|*)70T~(n!S7cfZS>@+p_GPyMp?3iTmvWGKLF?2`N=);xN^j~YUkE7`G<1|tX=auC;Pi1JK#ALI)LY6L zTI~eW&|Hw6sS8V;J6&l1!5@}+1pjHAFR&i_XxWyw(6p!TOKi>CxFYM6Zs4^0Mg?>k z-0vK%YB-yJs7&di3X1K+ZOPivFn+FwFo?OCM>#stuR7!HoRZL#X(OW??I(CSV_{af zC!~)LLv#=gtsQ;Wo!_{2=g0q~{`P9R4hQsZF3isMjJ;#--7beV{;=cgf9?RF|Cm`r zJnOct{bk+K4RLSWV%5CbjNdpF{dQ|$6-A<=itN~pAaU_Fyw^#&3?1Z>I7k=^ zPT0M~w35Syy4X%a#<30j_wW!fAc7KXd|AgaI5D0qex+3?$fQ`m!Lj1KB6?sJtv`^Q zfT8|WsDB@Z#dZwXkuuR?p)3CD&97F7>9|>hIiXQ{3?C|1gPf`ZG8yRO;h1QM6$C+g zsA1|L1>Xy728@Js5IPDY2FH4c+IJutAmesvMi18jd$2wU5`Hb%%$RKi#1Dy!RDsw* zqa3?(A(NkZeX^J&PJZF!ilZgXXh`y%LpFbLXm5#iYNQ-A9@*0jL#Cv;b}lev@>A^@ zW#>zx?P}{^p2}jG_MZT=wQYhLq`wiX5KXg-&_?p)@Z(Pro?7 z4;FrsxjBE->>iFQ$Z*rhkc5$;>$fz84oXVw+vNo|Wbnw)4IP&dGFYv+-dCR*%Rfcp z`UN(<_(uNO8j$A8)Z}a~htc>182>7m5T(?=vV8x^=Ja8%sLS#MwMmN>->VZO!`5YG z3b`I|%2lMF5if`_&`FH=`6yUy!Or>;x$`p_KUr{_!#8ZvhYd@+I%W%+Ee5GZpZuO z2D&wFYOt>ScJuc?93n_%|9CHzQdM7Hzb~&SAjEgTpyVF(VV!vk>}I3TBh%_`!O+3* zU}yCnye4KOdY};3R1N}X_Yk3>{v$^Yi--!sOF6_eps~U7&6hid42zjRe>6o~`L&t^ zN`(RpNeb6!twF&$hqn7NS4$+&6YO91_v088+I4tXrmVO%Kz1EI$}!%^S!ZRBF5)=o z$un+Y4Inv+z~&D)VlXSvDLeDBo4wle@RWxUi92bhP^_amM)2{Ay|ZBYL$tqW@DD}C z{s2UfLBJ*$G7u!iVv5LTr$tRIw71LxaHHC%5Y=s3vJ^>Dj_U|^JH=_A1gwLfLcx}n zh4uz_zN=yTDFM>y04bvF<%q8tWXDV-Q2X)NF1{RZQ*TBRZoN0RtAjc+xvwgJ=Vy!e zxQCA(J7N6LaB+rOYEEp;+MZuj-Da}^hCwtwzozOtIvO0>x&6@8$mFuzy*8j5ne5f2 zz)l%$Hl(m0e8V@9hr8WK9Fd`cI#^@EuVF0>04PT!wwDt+B38cY#7RAhp?gwTh>r~` z=*3_Hb-pBsM(OKqzkiX8#7AKT1N_Do{@>!2Z!X$^znV-Yp23_f)epnG&8a*aRK&!$ ztMsf5%{bSpj$Hh|MW^*ivBUbRj^*%T^avk0X2Qgw;bKo|XWJL#+y#3O@b)$%Td#YS zN*`}W&`=rwb}4XNpmz5G@X(3EilkIO&{9}zf^ucoiJ*!Y7Ol+ZTeaIgJZ;>B@kzK< zs(xwwAnw|JXj)|QxjlPrNIPPfmjVupQXS)`L9R!5+8D<);8@%y#{RT6H|p(u-{|N& z1Sj~u55j#_dOGYRd_$FBr<=Q7H>dUD-)(C|wu&q#zF3>D8#wFb2NFqe<-2XaVS2~0 z?AEQ>8#eqjFk$Na$ENn}k89#3-`WcJ8wj;)laXDs)pgqQOC*B`ekmpGN;`2aMS@4w z_H`BZqE`dG@It;=hT-txA=ptn>g=)D-Y~~|O#4-5YGG`%8car|QmIzbd<)~^`^3fH zZ&xk~D>z9gScYh*YhgJcIMv0*7jha-nKop~lp%ZbU0NeJy)6HZuB38$8`Jexz< zwFhvVAtioKn_}(Cr1nwZ9Ll3kydQNLIeSWnUQ8q~gz)NM=W*Cu93h^V6tr@v=Q-p2 zlq2lXhV6y3PL)HajSGZb_2I}4g2id)SL$vn9OvL$-AC2d!D~Ad;=Sw1d)D)Ielx)Z z5S_Uh>-azj`^%(LV(~SmQ(zfz(Kjs#60Y#I?J6OUn7`!zP9NC0^_R@7?@P0@Q(t&# zx@+x@SKj#vD1wsHCP(?Zo!ho7r$(H*J`>Wsi?`>TWBc#lT?0a9#&xD(TA=pi{HOlA=dC@M-#-Mg)ag^r}c7xkba zfi7-b=^1_I=3RHCPm@ugy0(!-$j8gW!Jk8g^;*<(j$=J$j%CK-++Xu#gBy$$n3ZEbMuj!p@(%uza%9vj{o(5l^pGK&~Wpu0W~a z+V?OC=$xAD_AG-=43FxGMPZI5D9<>kU};^ORt+?A%0Uceb^_XI&=fuT_-S7ldcUmz z?9|RK66wt&9o>`Y$WAKKZY9IcRBY^+s=;GMD|f8kuzF2315(Be=u%MCah!3+OMgriGG5r=@CPpV@^V&C zU8rtBIKm5%3tFgy1ub!++6-3jJ8VHAp*rEEtiGZdh;|9q)5Rvpb=*M^QdDpB$Jta? zbj8r=&KA;8O8iuig$zlFS6#vC-cv4zRjFN!mD%sVhs8L<1MfS521gf=<#ds;6K~@> z3=3fw^Iqp+<$8QjBMR6|&}a0Y9!{i?+Q+k(lWd5NNF2}qIa)Q{Lk)t2LEO0d`)URz zcMPI)$|Gfl8J;57bp)j(gecoo@Q{=Ns_sp7W`VX&SDkvIiv9D`&-o~B!JFuE@1pCk zy>o=^BC)Qn%~Vp{qEKpOE{z2*YP8d4ON2b8;ZENbm#viiw>^^R2WB4W7 z)lD~cLed>~#_P1?xw6VnKVCm>+~m4iWA3h_4Gpa%kk5bYo;%V*3JcG#Uzcq$c^M3? zCr;);j*N(iy!ZaW@v*_!s}GWw6jg5cVgJ!11!fDabU+@_5O z3yz;@)wj92dN2pE{*EWIAKU=5P$e;e>eR8~E2+x)_Z_3zrDDSAzU{kBYc4|@xaHsojoEa6<~+9&zU@Nm!eHKY`Cv%qg9%95`Jaf9k7;d zU9pQCF}$yv3Y@NP!S8GUxOm_bxy5-!{o`GZX06JqhfwR##;6j;M|}U|kzz>3rN(q6 zMjChgOG#c)tdKFgfk0Zs2s zIxe}pleC{VrjRV`fj23F$ECXN+_PfInyC*Ak8x|caN=+Yeb=m!-C2^=S=SjnK1H`H z_vk`|t1alOI_!Ge&i5-9wkn0H_;r|MC*kxafuqN!d`E zzx3m8ef+(3ee`|$^a+cINKA_I^(QF^%oeVy(xBH9oa1DZ(E#eIIM!9p-(EHgVKOMn z&eqp>k>L@E1SAg~JjUAg2l(F=8j0)3-kP7iwJcsiF8sr}c5oDp$SqKT21Ad^W0I$f0@1?L+0*N?|>~<`mJJ)g6Tag#pr4+x@RLrQ6 zAlZry8c+{A6g_LfevM(g;^T7?x0CSevpfD($X0xGYw^*|;^LJs<%u{ofc-`f^f+~N z)3U-%JP!&_N%G0fZ`Wj#LnkEuczU0E;&AV7rMdPWo#0t~WYe;Oj*L(|I%ET6U_wk? z&UgRF!6`ik&U_j>{@{t9>&zFPF@5xD&Z>`uE6Su9&+^MZ&toRd9erx)_U}J_2mk8Z z*N-YT*i~F~>co^i-|b!Tei0-Fz4rEOCr(DEOibOg(thdHf8K=|snThOx5H*Zo&Nk^ zY)d~quxWYTCZY2ITh9BL9>WHCT9GHAH-B2`^gsDsr>Dy@-v;=kL5fm)h=h4 z@ao&MXTI>Pb?K*h8O!rB?0uh!x;Bz;JS_zMX=i%~Vc+?f&a(KZqKUbkZ@1Rg)=MVR zXfE3*xZQRSq1G07@WT_|MR|Rzo(&C+e0bjMkt0EEBY1PkvNNaJKKSs@ zp%FOi@#Ff%FMS>y5dZH_C+6ojzWU;77gxpOPdpM4gN+`)Tb{LbOWqTYJ~!h&>O_(0 z&DYk(#Ya6nKUt>(xw)p#K3vbj##fg-9ve&jvMcws7k={bb)C21zG10>7tXi7@Yi?J z#wPvwuTy@?DE-HqpCt{NI`6R&K7qh&03W{lLtftD&%gY?=xFfsrqZP!ZXA)8{NUVV zg$^7%aAxTTtCL40JhyOiWes?3(NY-^z2J#4{bKQ9x|%v*wOD+7@Y%cP|Gcuk?)=~X z@7d5$83P!^Q5VWAum5emkB{rZ7bj_40Vyo^CkxnDU;ffB5dQPsM->V#JNwLk-rbsz z6#dxaV`&vQcC_+8AFYV#AN9(sGi^5T{=YJRTzM`iihTNy506NrvbU9f_VKnyAD=OH zqBnp>63TzAc)_=euCDvC!nPaTvUW!%7s=3NLF(>98+6m;LHo;~c&L-?F!;FGPVBEK z8i}(>xXCaX&0j75YR;SouX6U{`-?Rg@bM{yT1`56zO@((44~9%x;>?+u?Bq`P}tLV%EDR< zW=27)NvfM8U-6PcO%NCJjohBLjR6WuK|88&c5CdM(%C1lJr}8<6{O>L&RL8dweVeT z?$m`ITLL*to9SY0?wrf~(;KgB_qzM-$CB-JS@Rda{7v|Miys=I?JN!Z;L>U}<SB-$@w!Uz+^H})TIf)joO^$#jMkjLz6v*C65*rwpACvy8jig{Q7@OKUxzNGes^} z3n4MdjxmdWHPsmby2 z*m}{+4Gc{lv^V!?Z50z3LLJIGE|+Vk&q((5$L$#~Ak06==X`kuhk!x>g`KWh7-X}uv{I#12<|wLRYp*>isj#s7-ZyEc~WM>F{=R0FgDhP zTwJtrIWSla`A3fNuQ%y4$NKnS`NR$M3yTb_t8XwH0VM}kvqdJC-Err*;iIq}l0wN* z6u~f7Y?DcHB*{wj?&_Egem^5R1+wd+TI-S6!ERO^s7KBOyYi-6huC%3N=hcM5^nZA zcpc8(k!rQN`>%9Q1dY`nTmC}{bRRd^v)790o|&E4T)r+{&yA3)b@HCtrqlJ(>fY<= zxCAdrAo#SpUWyUfz-8y^S&X>L-MY|2hzsHh+Kfq?4MN7q6F0R$Hj8%dH}T7{n!239 zuNyc}O=I3-6^~+ag7Fc{$q5o`3Td8dE3^nO_;@SJkR(N`YYEC| zHd}aJ)2wf^nBm3^*_D;04UKho-g)1Y=}7|z>u8}jB&nrnqn9^@J)v+wr2vyB2a7A2 zTB(#1at75{Ie!FOS?EG5tTv;}Y@{qE(AGlvdByq$0G0&=VdP)%6ts!45j>;PxPZH7 zrON2M)2Gg5W$oFy{lxf*4G%p$!rxDHR@6pdzqpvCQBWrAHuEn@&}!v`2=4^K-Z+5G zX0lpaNlMAnahsWHZMC?2c=-4NAyBB5Ady04X>GNM55YELOCMi)z#w=9ynVHwBb zbB8jLly+vXg4>beIXBcEa-?%Om7Zdy!0w(7FB(|I{!Q3E2i*)cZUvI(Ozb2db`pM- z8F=-1yDiMUE!D`9zvz{31fRUR^hYMpmr>MJ)PBiJSCIu=kEUKllP;&0t5sjAM^7D) zw`TLJ3pcm_&TDiv#O;J9|)Eyu^9gw1-ABngsW1#UZKL7_tu-FY6*mv3PJ&+s6Uk@Xai^Xa(vhMEgdopOK^f|nrz`_<*JRXL7a%6E(+ z7*1Sl433`XO<*RBcKlvatCcfW2_ok|~*y08X zeoqRIm7F_$COkYOA|jNgft#C~%~oa*&i?W2%4BA&!oDgdM8YuwlaVtTfJy_Jo2~hW z&jKX(5BA}U3JmnqXsU+|9g==usD}qu$yhmXJpV$)`Nq-X;swtV*cB|*;gHs5_sb~k z%r(~P%?5u;Nu4bRdHagAI)$efP-=jepKEbpX~D7jDt)}YkL)ixd92!dtPg|0iNdn-^5&t*2|Pn>W`NboxO=GNw8JV>M)BQjYS#1h zh{Tl;V<||u480xfjh)@F?MZ29BuMDtX1sYCW7G@u6Pi`r9r%< z=pFy!M9CT(8rQA*xvg0lKRASvffhsS@%&SH`%iF?m^J&Zuy7SgfWQDx#%5f#a%b^r zAEU`yPefvE;yoRO3Lufw^fCRs_q_fvBNbtlfD>JW6Z&kTApE=!lIPX;2 zxUhg=pp=2sl&Ia?_kI4!#`4mM5VmD)%Y!!0{xOk!#_v}hIG%qpF>$1CfDgkkd-oJt zER05D70z$+WYl7@8ClL*qzDjZjzieRpWqA}*G||NuEl7PaR{44B@Bwm-g12Te>c?C z-ic7#j_vy_ZL0n;{TLynfsEqWVH+Xnf%rl+DurCewY4>~3{MRqUow>RcUR7|V2|t% zNg6E)5^l=Pc+<3ZlKZGzXwacFh`#}PXZLXL>FxdWlTUyDedhYLKdoF<&mo3V$Y{AX zX>i)4DQSZe34wuu;ll?P9BIzlvUAI}Dy34R*19t+!?D=OFew8vncQHoY|q*)r}Pv- z4oe<&*FB>`Bk)PJkWlSY&&*r3@_+08w_)wt3IrJ?6`n9?;PmNty18MWi{d)n5tqy{ zO9+HbppC7T?b~y$T!mWUb;q64?tduOMT3hSl;C~utf?!%-uC{xpF?O34h(x>_QL}P zV5M+yaLl$X2R>i=Er+*SS*^xBH6>-#m?*h$W^>@c0c~wf3MF6|93xab+l3J8qID4> z%0Rp&3xT_b%itmLF$4ODaXSVK>@$1zy~{rT`klA_%`;#hpTJr7J~%kh193n>124}$ zq6Z`~T7#RLt6HNp8I4v8pjC(f)ja4)a#4Wc_oLWz7>U0G4gr?y_xn*|u%luIgf!ZQHiH zY}>Zxe!utLtTk)?zw6vQc`_n)M(o&eM?s5r3FonH%*<>)n^jx6f*SCMxW7kH=z#ZZ z=DTpM8y-eZ?)7&1>UBFq-C+3JSnzU3`)%EtPzFoD{PUHwuzF%p7ZvXkL{g@ZXuvz0 z7FIfHU0N;$FlkNyr)*3TF>#=N@%6W+~KJ|789bIRIDqcjcaPMGVIM3mgu5Bm9AtT6cH)Esu?e&kX}zqc5}5f22q0<*8;MeB67?{F$oLyqs)cy zK&ONk<++x<49!fyHC)9|p}gyN6UNOB&hSfI@q)-i@tRK1TlBcVr=yOd4SB&~OpiQC zVNC-L($Uw*_o0Nws3f(kD3&2Oxy(#LJme6$jNrLJ%A(=mk`yG;IERq`5op2yM;k<1 z?+}w;##Hl3@-6F6m?X=XrNQhuJjbucGTT=ZKls2d(F(rpSp|1;( z+1$t@+uC}{s!wDDP-HD%@H$L!DI9sAQMx-rzJpyt6vBg$f3@%tfn}?-c1p+*kHSQg zHaxh*5P|QDJAw6ULCw&BX;9S5^VTbfx}brI{SZmUAOL}93=?zxAcz}Ckb~L0Sd9oV z#Q_aEVT_D8(^|xXGFVMS)BrDfKgJohnj(Glb7Y;mnf3DdMOQ(V6Lsn}Mnl2}J_v(R z-&GER73n5!=^jd(9KH)N%l`q`rLY%U3V{A!W)Si){ z76t+7^;Ra8v+g+79}fisg9gPFE)pJe6~sq*{AaAk{QR(MX(n@Xa}z{F7!ch;x>d3w zyKJr(xxTsgsscL`IYd_rmDxM*M#+u3T3LiGt<{3Spi#&q6C9M!bnS-@jo_qr&4MtI z17W?M^K(BJ@M+BzY2NH<`71_IJbqGl*WKx}g@D3G>VBsfnScZ#agx9wmSWQR^$m)| zk_M5SPsq;~UXVuAB+ssWwXjl>OD5cIgf#t73k}%EZtG^SMfNBqU~#}8q1}#b!&&ZC z))spP&dkf17~|Mpxkd&qD~7v5HJ4wq<|5Y_wKYZqpE+mv=YL{!aa)7= zRv&~Z5q)oACIaRBSEO~kKRc`7{`X>;dtPUp(k)dQY4uK5HS3i+#)rZV#q@QlUp{#T z_Wi1<hPAie(x8JLAUP# z4Rsd(yjVyOZS_+xvB&vCRdQKr(93%8 zCh)(W8Nip?A*l&Js@gkzCJB=!!0hDs<-QYob^szOmcn$f<&JBlv^!@z8mha|N6d1v zlU9ZDpg~{t_Soxra_47iuus)qeG=44@BJa%3ogHj979W<`~OIUM!*1iU}{#R#_edi zp^s)bN3}-pL9&#FAm2b3f0x01Im7mAJxb6-t)36E;H+aUT~<+Dn&r6WyH}XU8F|3w z$mdu4ILvC+(^kXO;F1QxL>_hg6o5B*KA$Li)2+T^A8}=%R-a=IG@ zSR9py9WRixAJ1j=`z+4BcgM9vZ`NN8f^6FRaT}?53>QxZhuz%gwl*tcm-2^SAh}{z z>rSksF1-v+CgWwbn>fl2<@G-M!I#k2r@?(@o`X5d>+yU*N}4J1=@62EL3#Ua1-$U8 zsn+erf9E^@Y;PqeR*@%VmgCs5-p6=E`v!YNM*T)VN!de{L4<>9RT9Pggg?17R!`7W{ zr{F993?{hIn8$_g!K%wr&r%ya=yA`TflfS&pnj9>**)EphM44;i&dpwE$-bbfM$8{ zT-Ah#tpFyM{$hbcz+6a0?>7Ksviis`U@}hRuPx-eayE+jk&CX4V}n235$7XJ^*kc% zwDEJ%GsXSoIByAH2$6lZtZg;l%+z_e5`abJD^tivK@gcs^c0#{u%5#9srVE`}dZ=U7ZmQa1x@%H!}BZYwSSqfs%cFu@{Ss7JT3 zF{^wOpIVtQsoHd$pfp|I3^$XPjZP&k4l4o)j~g%<^;PildXIs=Yf1tkZ~K~$d3=-t zwXhiXd2!958j8ziv%ij^^3)um{OzXVtm~PY-%iBSNqigE{$M`rso~t?_i;do5Eh%x z=Dv(Y=)Cq`jJqCU5?cyWX(FWj!}#*uiDH9^;PY;q(^f?k0#((?Wa9VgC5O#oxP7TG zk@&SIz#QKUm*w%ZsY!bF0LlM-!tLuGwqb)ow!3IUfO^2=k;h9rg_1un(i!%Cp5g8W>oU=cd4x z2ZX>~_S1)D`kgsEX3zD*J0?B8Sz7CFiOCjL$P&J*(BopM}zHAG6ro)6z2sh{8jqe=c4m4Z}G$ zJW#6P(ai;u;qzl5psQ$KHy}1n?3RsU#v`gWG;5}JZ2~3}2)zS(mZ1S>< zH#aH)6SuEk_7@6!UnBEb8U1A4AROwp;aAywQT8}8;!=!)8bP66~%H4xlLPXZ=|M9e)FBoov@9y_F z7G=r1_B61HKnNU1EI#gWg7W<6oR=AHxs)c^B>s6O#(i=?i|btHm#Rw0^6p$(5tIo-Mm*pni-0pJ;;y?ccgcS`x%2}D~h zf3-QCLdd=>=Q<{rvQ7`xvjbcGc_guk%gM~*6n)e3?>{-u>Q?cq9#$gA=bG&imwOb* zTW>`~PFcxH;s_U&6SHqNcXltgbHrPtZri2cV4}}1Rz~SJf%{gQj~`lzmrWRWRVeN*`spX zG~Q36dMzFSrT_l4=)I9UyyR!P?pa;vbOyec!QHpxtGvCbe&-fpbgYeK)~@X?t3BVG`+g zibRJYx%~WDaL%47iv~rGPzwH!$+06^jxMDW5hz>^Lj_Lu3G{t{gfvf85{r2=XL0CR zJS;AzP#xo51S2+!0-Axu@11KPYl15MOI3&t&AXtEgn9{)4YLXJ*Iefg>R^%!plMw4 zwOZh+1{{g~@UjdjS!r9rt8IcNZ9&?AerkdZFEMhPV4xJ-*zW0zJHVth%kk@dlF$aacATFNQQ*C z3naFY3q@i`r4;mC-JvHV1vVa?sY;xb?R3))#JG`4jYRYQh*G5>c4_*^lZ9UYpmV1eRg z1sJzs1$|Y1dhvP}93(}Ex{yM`Adt2ZR#d?pFc|mx_?+!4@fXzO zCrH!bL=m&st5gPd_PgG?9;N%k7F5sj68?VK9})XdI+z*`m;9{eOi#QS}=8iGFC2{ zqkt4;^uZq;KuYO-yX6{mL1b&W5Jg*p{S|s8qj-D_x|{$1WxeqI$9k#Ry5}qp1rX|H z2rA>y?|)`I&g9EMq$*ceXCMQofeL@>Jp2y#(o$_+XGha(e=JGzhmu7xn911mT#=vQ zI~8;rP6&np6QSHayhl2oLV@Bk7zL3U6#N{Fq)+aL%?f8g30cjWoJP-MD_7K57)_@F z=tGm3d%%*)VlN!e6JRR5b$K%@HsisWzlH^(K-%l_VOMU}7~6xpgaM?PKFTWwL`p@; zgR63k4SCq}R*}yH_%m*kx^9hADBp9Hs)JvlHWa5U@3V-MEtuZUMCob(?*c%Z8$che zhahMiGz}te$80t+kS1=nJoNL(5L~~;MKjq1!;6O%cW+m6kF#p#R7)2n!(=NKY=J^q9c)J^ysJ6AyqKBb)zn9c(6q=R z^VSsB;OMNhRyxCV32vsi1+uCg`ux;hLg*0%pr5Ks^on9@$ym9u#G5~? zfwn_FUv4(JZ5^YsL2PRBnv@y1B8n^*Yv5jT%RC!B0kNfBV@By7P^r!Tzxx1Gjia-h zJoO$A`}ANs)tK>!IO+}|*P??y){km=(Ykfw_y%I92mk9QA-|uu>PYF0iGe@%olwPr z*P7T0e_0Nz`(t_WM(@unzD^ZjcxT0!#Y*2=RGb|hcZa5qAZiJ3AfFczx2e&2PKvjB zmp)jeP;ED#_JOFn5=GG}Lx0Gy{cO(L082@{z_jL*B0^7vjI=*{H}3G>Mzxw^+^j+v zM^rh2Eo#9LnZuQUSDwV(oa`hF`&}|6FZsCn4bvP2m;zEpDB?Et!-D+RP6xie3A_i( zS9>pGu77Ilf206n*}k^U&KF#ulx{t4PvME{6z|?|cVrTj)GpuN5@q3)y+Yv!KVHlP z?#)5|stWAcuk^ReM$rEi>yJu#n(Sy)&ADU8@=;M(Qm>um@vgUmA9c-pZzHY>(Yb!5 z!H|HA`;Jn0>}laKE6)1tF|^1FOgk@x z!v+RG#cB&_q#XRPHEI8qsTWM_pWL9XNccKD-im@SLY6bN4@SCkyWa-a=xIx%wT+{R zOtoUg7WLws17}>D(_+AbsG7riB3LI{J}(YzAN9At3jIg%8120dj__(bf9i?W=rOIo z3(|U_Gn-HIiM2iwG~sQ<^BkHd-}mXG@$IpXbGJQ08J?@b$CGs(|Mvc6ct(sSB&fPd z)U92yZq^DSdfkyq3;}tqok2}OHDFGsI-l?0lJce$`fs!BK*6`0!GVaDD)eM2H8BKz z_Y6s);_qD5n9E%{a-x-9T!NyCyo)sl$jz?@e)0-yI~ed!^^S%c^)Ww1^uFTRCt7xpFq!bAPH- z+#Sf2YL>?t=9+7EHG&nY#E7&LoY5+~uot5fb666^g8xg!Y=Y@H=UrcnLuDIie6^~_ z8wAUnn$-PjvskRMCeB`Af?7gHcP)O}ZhHh26kk*9>qt=#Grq^b-Y23v3i8k3k(Gb$ zOKW<|C1kN346cq>FzoDp#@)xNI*k`^wIC2G{$GJnZulKO-Bmv3+)U26veHYWhaXzXMeTg7kLbykF*pMl7fCi0*={KN~7G+(Wvf_@g z3!(93KAYUS4pi)N2MaYvUDIrcKb)G{${e87_6q*2Rn^4hO$wEDX!pNtlV9)cZT#(R z+K>aP;D^Lxrzv^tVMjubBS+DypCd;vPp_p{IDG?ZKv4qn{}MRLgyXFy8n#mEl#%Zc zY?munH}+dMTgAB)^RaaQeNYZ_21w(Uun8#f$LdHB z7RWY&k?0$R_?@HIOm6M_|DGNwIBPwf*H%@^+U~3r2KvvN`qb?)eRs)%G>45n3hNuZ z(JcjrsY|2unm!;9;+YYxsur`%>7fQVAx&r~qH&@)5GbuSGd$5Sg)vEVI(Nl{?`J+x z7wp(SfItccy;29Eqin`Sg!Rl_<#UVD0q;B4avAmwNEpp0NWJ1ObWR$H>6D7@a}0n8 z{+evoS|!Ph70Ze-r3RDaDo>n4lSKx51QP@g;29+e+6^ePY8*Wa)s3bMnO0X>T31^! zqi7()hqq(YcTMkGo>3QlZGJ_BM9YQkr*#9F*WJ+MTho}cKnekG68;~<15TdYD1d})}m8|?@hq`xZ~~a@p`|aGkVCW z%>$QSFAog56{6d^RHzQ~oN}J@gQBd?6^3b0q^i2)TgG?+Jou_ksXS2`r(H%uQJSS7 zw%Hb}zvGNV$0g8dAeJb!3#wTn>U!edAq#87z@oyMhV?HF^{{3-AQL<{!8R@l!2fQfKIaO88jq z72uHS$82!YJT}yUQi0fd8~befXP0AX=)COy%qob?>bm`@=!0`{R$S|M#%6veGnYii zY|_kr|ND=YOYO8<}Xx$rBv5ukG@p-u;W0L)iWO^l~EE4gV z*G-A_T#6OIscugIa#{~n8Tyq>s>&Lj0OE*80x$ksRqTOYz3ADYT|%lIg%L$Cl_6GS zm68ZjbHL9|j_R*%jr|Ote(!|MM!pJ%wa#_QWK)RhoDrv-ol83LL>TW^}&c8Isb>a zDae63mLJBMu~+_Y3_MLvEjczUn8Koez@<;LBzc38=uz~YblJZ zrdkE@PbS-%_U<@9xV>&j9&aoreve*q?Kb<$jaQopt%)!M8OQ1!#q+)j@2^OrkPH73 z-?lF_Q|bWCii054og7sgkK-tHx6`bIC-C^*V)e%6@bGol+ilmQ@jnQ;-qs<@8sm&w zsh0BQHfGjZGW+JJl>)C{FnJd|mR1om2jtU8(5(dB*6Le;!K)}icQk4Nia<=9i@4gB z%p5E1KRT-3iv!0MDuU%8Fq-};^JyWhlLU%FaTb!`m1aidve#lFo>fNJJ;8s-=%s{;2@Qv)MCou(AQ$6YieF$YJ0& zX#~!o-wY`wbI5v6P;o^cV*D2Q{Toyv%RxO{kQu8{JZI&u)|2y};|&HD%2m>TuzXSx zMCV~4u^ze4dj5BO+(BB+YXsJs_pG_?_ChN27I?s}lzcD0lngqhfWOo5T%sA{P%%k( zsSYq|uCEnyJGE}Y7Lk?3x=e`!MY3i_O96jo;Gz>`QNQ70*+Q8r^C$FZ-PpjD+ zwClbmpAgCo!d2;K?Lfi`jDKS}o=CvWOp>mmcK;$ZZdpr3=enIV9+sm!PB@@B- z!L&-!({!n=WaT2;EOEBV1np z;;Sf!;D8A4YMTWyXH=KMm7pXQ3l}nabcid|A)a@@PA#seC+ofH10)YaTLf#stWu=K zANQ~cfVi`NCl7 zflh#Iq3r=#0+ojS+fx8v!>{;1H=W8Aa2;*CiG{&>yFdNk^mXfG8Cn|F@-}NNUy_Q= zb7lwn@5r%(?3%9}Zh3f_^Gm`(3i(~NJ3M2ILa4dfIBT|<=u8^DqpPZRM#O`t8_m}F zqz9)dp_ETV`Pr$Fmf~)554L;Ie5dmIMJ()Prw{uRdE>waAGO*T9ZubpP zRYk|oPmc47wx=c;F*KQBFjA_G3LcbIwgRSAv(?r1*IQ%D)5`L4%e(zmgWD?id5ac0 z9aRAwB^7fhBZwiT38np}>&#D<^NsH>h5)Rprkh7}{p-$yO%{Qjr?-)(>CVjW=Grs+ zj&{gjybe-EgcDYbaLNMpCS+@iQKYIRD#jqFVR<2F3+z-bZW0+8+ACUzhxY?>DEz-) zhnc!8P)#o0pR+r6xc zKToWv$e7<>58z(Ciy0Oq3n`B*eC83qz^FEH6C?->q}&+JktIHP$oSQs_^bW?j|woM zExM_CrkS`-$W*FtVmw0yNBss4fzY5z4W!1;x9{|d?U7#Ic$oZFq~B0 z4Ts~jiegfJP1!9v^ic2k1)0SRPXq{2+oA{ViSkVPyG!}5qE%aGa`pGR-S`7&G?zl# zm7J%}m+BZ?Zzz!TJx0wb=O`q2q$t_o^)SQqOH^|STYg8ze93rsRGgbjEAG`Mx0CCo z=`9{Xi#e)2X*;8fSh~)P4ELc}2-*@v)K;TA3)Xr3X!qdh8R+*EKl0&1NOgG}z{tPH zvy?*XN=n%Ze0Cq3qX0v(d?(F`g9$F+o7JXPS{{s@rMY@~6oI}k@OoGUP1T?3Rr=A6 zx^6SRGl`{vLBy_R1?XS$QbHsml+Ys@Ti1UA#xt-%h0+_ z78+6>f}D&+9W-+FbfmdMR{7yFMMQW)O_@SG5c@Kdc@#zk81?z(54CT=MbhA4QCR<) z>f%<(A72;1qAcj6@RM3F0fRzpkdQS8ZYf&Yl^x`g{{FYIlqA9*P~d?Tz?WX{+o3&^ za+OvZZ?6RqDt^a{L;<=(@KqaX_Snjqt!`(=zqOHIoPqUmnXL=_hchJnJCSs`ier{* z*Q#XGdGeFLL`|21ZmD-Nde3G`_1jN|4R^j56HLhepuq{-99&$yU|@`EnXbKJzrpVK zeAb-I#Wq@vMN$4}L0XJx9xOa!6{D^K^gY~89&WjA@c#N8bKWkj18Ftyx4VZ;;~}A3VsNkTwrOEw#p&U|F-RH(QURxN`ap&JSGug&)faSN zn7-XPa32L8I1`e|#ZwqTqwRF*lQ{=*9yEM5trSf@yLTIWDzde^D3z#(5E$yZQHEmJ z)#_rj_(o^!3`s8~g;ER)5IP^fpT`*T8E?242Wr?@-3U+gPb~6`VW-Y`iq6~3pu2&R zY2T0{-6Y}Ln$x%WT-%#IS8WE}+TNUb48wNAbLa4Q1N4y_3WzGjkWyO zH@#nzk4dN!reg9C1vb=JyU?FsqlI}jH8rF;XfUC1yt_vTA%j z(+OI!=t(Dsp*}#u`n^AuP5Z@XrRjyqqqG8qUs|U&6k6SZng;Akb?;U%GKcm6h&&X{ zh#iZ42x<=g0f`);2#^?SwEl%%G;D^9Nx^)~fHbwW=rDPHAp@KpZQ7E@Y zyeKUVq=?r3C)jVs4{C&6Scv99C3xqIkRfFoK9p8gHX@zK1m`>+1TDXRX-#rWUfY3aai1*t{kFmQ2ET&n?f{kOWU}(TL4Aks2Q+(EAWr6cQey zM}RVvwp{D#-Ik)UyI`y&N>bCk?M7XsEmMN|sOuMzHF+MB)V``=guA~l;MKkLP&*5ky~P&`&5mxlA4GXzA6L0a!eru+YQpt+dXV0@i%sj;SRJXK&J zykV7PzsA=%5_NaV#=gOo!L}nK(!nz(%+4=u^|cWu>5*STu_Fx2m--q9!L( z{Jy}hC#zxq7nk)R;Jww`eaj9AJxlLoVr1|8a+_6K<&vkYMI!XZQU>z|KXvcsBT zf6)byBs=_~x*cl3C>VD!g*!=Sm+;smjuYlZ;QSWeyblKvA#Yb1G3+x%3zTZnntPW- zYi&LXe}JvKi3OFd>qIVq2)Fjs@VI9Rp^RySE8`kUAu{6h2;k-cHVQ*Xot`!SF=veH zY=ZVUc?hhIy!DzO6(}3fA7*up9zx5>QrPEkvUm3p${7hLr462g@_z0=U&=e|ACmF} zACC3^41OL!op`YiRyG^C2f?a)W)kH)`JY_?Oyh9EpRLQ;NyWnvJy}UN9?OekpL&F9 z|90}Y$8E77ndlsaz}bfKG&SMz9M$R}T3b$U;=l7WX2P?iYwlOFc6l>-|);T~) z#VBWcAne$zCL{^@5Yy3N}!+1>y z{DH$@SK1WvtI99YQmcW{r*zC{v=<<^OyKo&hPbU)+}!NS<{snu<=PC0MCy9n?gM0H zHAYW^Iiz}krBk+Y@)z3loI)_RM^%IP5xVQ!UgTAT4eS*`WE`&!0HBZ4kw9HhyHifW zEj>1M^cJoYtHi!nr{$H1q&Za_QYOkq3tjZ7AiQvJggDlD)W)Wq6%&=2@aCbytP_Yj z^jM3f24M(X-E=?&I8dvj7gAiG&4P`Qwy++M^u18L&{e`+e;0g(7Lk;YEJ5BFaZJ6X zn^=K&2T5lq$v-lWvTsPDgBdzt z8tJ0)fQQ$A^yac7shmMdt@qorXLN^UKtg7~qtCo>oXPH!3j^^dJUK{3g4 zwPZ$d@DlVV>)@~NuYQ9@4E>e)bN--T4^Who`$D{Xe?3xZr#km#gRtHhl~$GC z4*vZKkkY@z(#_ub_`X}r4-1Rj>A^0M2`>?;b5|dWDqrzI$IyXQ6+u#C93U|^(sH;O zC5tG6lqw|HiT8WBXb(5dit#F9n6cH&br?#bg1{}YF=aCTS4aB&m~_M7O*eb4YaN=PD7F^)BRI*6$5RkQ}om*eplJ0p|&KiA=;qpo75 z2_Xzuk%|Afv^2&g8?v)@AhZR9GZw`xlC{J#W-Yyx_~0or)p(Nu`v2r zg2~0%A&x{|u5G^2Q6zu#aq!=%1{@ui`Br#mrM37RW?fcTjRD=NMC% z?ri#74EGq8w(=yWM3j6vW9#&h{#1DoBd0-lP-m>*T>QDVz?5wJXwJw0m<+A5i6s;` z6LaV-i~*$_xHlM@C-ha%{`;93U%yW!A*zmM0=Mh^V|Dj>(L75X(lMe!+Ur^PDJ-7} zq~2cX5itZQDXm9TK0R*o7;^6ctiLR<8CufsXH#5}@|wVjuSah&WnQ`dr=T&{LAgot@b>-j>^4^eG zzvfxB4JTAy_iPok=3;GfP;qQDr)W_VraC1gd9VFE9409+cEpr?wDnN+o>L(Tvv)}9`DF*|Ja>D48e5=z(bsvjtCTff zO(+w)?-gKzNBiDY3h!5InSQ5$fI-CK=u@Fa{S>K4X>r>tZQy}ohuL<3`5Lh zbjxBmmA4^I^gy^CE{A;T8PDH#FD12GhBEbsm=1$NG(HidZUq4CgX$4y9;m{&_xBlF z?NVJ2>X=L*$L{a!R*1!FI-1%L&omK50rv5Bs^R>swjgC>Vi1gdCqlVL*7)H8;W}Xn zw&2Z5r4Rh48e`r&MK+I5$8bYU&+h+2UDP@(Wj{jQ+U10P@ z4zh&2@8`&OE0f=tpGTT6R!Btc14%T%k4u@=BRbcoBnAOZQ-DL(BmWwlCw+th2m-{C zet9%2AHXe&XpY@Le|qb40*)%)eNpTCI z%d~p_5prAo;uw9rOH2+OQB*~zs=U-|Cvmx%Ep2vnL`~BVhPTdiXAhT-KALE_7MqPr zW#l_RGkL&gRxS_RRZxC_B(!0cMQtb_WxmRx-T)PURd9YF{C1e_|AM6Ny`PAt@31)i zm%sH#a_|~rVbNBae3Uk8&n+!(Rq4q&QXmNteU?Y3v3TKaj^La#$ZLt9I90UAk(T*M zS~wT){!IdoP*ZE2!Tf@+=dXy1in?|q+BG7}Y=B?!?R?`j;-{&DHLT7Ho7Z}Ki&9=N zUK6Pfb~G8}On!?THQN3{Vo?;{sYqLz7qei2cj27JL;mspet`ki?&sq6@?HN(>>rKf zS^_y*teK$K#IO~c&%`E7SEdOC-33}ux`VLm+_KS_vA+_LwZhHg$4L1h<1}mqV<_D9 z;*to#0PLR?aVr<17NHsQC!{Q-&L%zCdqYtfsgP)&b3;?AJDa?R(-A!@Z52YKj|UQp zadFs~mhq4}0TVz5+6MB_B|cZyZ^=BMil;c^4tgZsT{OHBzI_ z;1bYUfxFws=Nbhu&8DsfKYvtawYhY$gL??qYR#=~OS-8KRweMk7-be3Dm{;_)seEU zkL&ZBuNyi?U3oFBp<(WE;Vyylw&+_<6I(GHL{>K!_f5|g2DK`U@7P-3?)K)RTK^Ag z&m|x!MWbX_aP$$DOI{S{cifOo-HDA3X5~k+FmW0*8J)kt80F@Yt~co*A}n&pz$a(g zwji=*mlQU;)EehjcHM88J6*Q}56;ftoaNVvUm`>b8Ytzg-(i246z}9N51Th%X*|5@BT};!#;Wkzy`y}$e3kv_RU}Ygk zD?`+A%xndY`fX~p9%1&jHCpurc&7Py%@;`}hPB$1e+;zts;X$AKpov_`1NSreTI(V zb_`9o9&BTfhqKB828L6(1hb0ipF-19dE$9HFc#)SLZ9LlCCDOYi_wbw^wTs7dUezD`Ere8%VWW^H2ZzbIDQPJ z=ly*a0|VV1RrPcLirgu&j@HxM{o%K|01@OFak3k;hXD5uv@}UO*y|u48l@#sB88{H z{0_aXIa*4?TAy+CL+j3#b6GPRYQ_nxr!VV;vR6)np^^$q(`gV9mGofcZ}G0)8fjoG zegX&taV_8>Ej`b}Z&;FShj}V~w3Xh6PkaH+lg*2OT6i={7egp51h%jH>rM`v)!id) zm%!|g(g61ail#Ora179n#zy0%3e~%|yGw!GkH=Dm?&A*Y&JNNKo^`v|;}u9a9GMUX zMnVg+JHha{1<+_}Mp*BeJm{ivRSRgUGhH%2CqYVZd58%h08GV@KCSo@o2aOSP~!3} ztBXfQdBkRTUpZ|K$(-ZydXue-|Bv+b43)pY8I>LoG(|(*ckMcC&Xi2?5*0KOeRy%P zC>hvog>_FUSRLpG>l#&mchM|j5V-!$w8rnO9v9TZe7z?=Jn@(Tc7ucFI_HZ_!ur78 zt$Pz=Cg;yf9DaEOg#)O67v8B&MrAJZ3&Z6pVrzG-34!(Nal)}Jr2xeGTk@Dw!OnCA z@$u&=!a!Bu`je2*fR@16epS{aZ^ie9r`d z!}|CUd_ZK0mZ7$ji7BH|)v|*RpJ{+wKHt&No-Q9uJqEoVfp00nGtS6ql@%!XTR@&&xy_ zncHP3j!J#FkcT_d02jr(9*36tkHJWuTk3St*1g0de)9Mz6YIGFDM*#)nt`^V-lP-c zDH6>fC)4M@geX;4q0yG9#_u^FV9jMRWl{|x$!6l~(-S<`Y~yfO5}MaDqo*OGl!mP0 ze_;{O(^p7&j6RZr1B!^VcVdc(o38&FI*{&v|C6noxXRpgij=9TWH~SsUD=>!bE%wW zE@*KLs1C{!Tf=Z{z`B2!w6c1)wbmH5Lx&1DX1KL{17s-m@+4J*qkuW+^VzYjIE_EkHe&qkLUmXTSDx+B26CQyn&zn;}Ut)^$7^taL^^fXs_Fwh`>?a}qaE zo`E{4L{P)0yZEE1u+4}|=Z&UpBjnV8pl#?)IOEgRSq}_|mKlPr#8Od4B_4z*Z_O>1 zC!Q+y_CDYL`UlQ3Xy1)c%MkqR*5T*7CGrxV>x{4%ClaRlGQU3-29H|%P7;H2B;0~_ z5YGuR(@CL5B{f06HWbSz7(V2svgCFU8dJYu$CH{f)qQlM-c(ujr~Wa!HL$?qR*G^T+`J$f2TcBlovdMtH`6_V~e643&xg z-2oBYM}xy0ieQU2q$R5oqN5&{b&DjW;q`-3&g&fRvg*?hq0_=NNb{r7_9`SC3JQ_oGy_j2uX4vFjhN; zoL{tVXg$MV1#z=Ko|Rl1g6w$|tx{>e886seOfD<83?E~It;8;)4eW#ey#Jh%c>}to zSEbbTPT^TVCi}<9{kII?0sdaXN{Jj2MX)E?*_6Zr=`ZJE232N7w*YwEiZW5fW(NWt zW)%7^2n4pP?iY8lB3QXxm?PDH3JldAEbLA+w6DUQQxJrQTq;}JS&63QZTfRe>g8^m ztDV;!){kRMrYXhL2ce*Fb{B_w13`k3gh!J^v(0}<3dqn0&%S};9;jv237mtQR*;}` z6;3oz@JeWKEOGW7XG<;F_2rI0?KIMy!8s1@{lS`wP{d(`EE{i=1hVEFD~&3Y2=h^0 zjm%}85^l@59Ct1vZaj2&uEFqev$M0Or>F7pHz5nl%F2q0$QT%IL_nkXv$}lXog@`F zd2|_fXEECeKO=k{`tIs~XC7{YNGnYdhP3r#cmam&P5csi81{v+XfR+D@-ZwSIRNNy zC`JtDRibuuWONi*7v62nNs7=e|HI!{!rxk-Q(akd1(bCp(e@oVa)M!COcw$ys;fS>1?&?Ww_99YJ27+uhn_c#E(c$d=yR29Bqx1PPzQuIUSeco1Z507~jOj*^ zNljz|4KK)o(|n0e0Lf2I^?HBufrmd42g&wn*PEn9@h$>0wU-B3PG!^LoB_JMB|*_%X#} z*rBhs6>ABtGP#^#5;TOi!n(dX`=fx#(|LPE$DH<)vQnB}ygYg_j!$y^-qZ0m{xkv8 zuTG+lC`riLKE(e5y4@R9z=)SG{?s=YRB|`rK86(bfWjn?dtjstMT2f0_N8*dTCfdy z8}0VI7h#U8dl2iZi}m2`B4_n#*-b#<5XyqklM&sB)Gg&b!!^@bMBB%>mlP&`JL+u| z-S)Z<4873y-q`g{MLp`y>3H(@yL}z$3FYxYyR*6dNnMWMb)0PVzgDiS0;T3bRv=wEs$W*r5L;phg~yvVkkgzrxWe)CbZ3b0O49&e*fx}>$J3H^Rb zu3`RahKsYTJhBTB+PpX5B2J$uzeAoTHY==QflQDihP7$uRTcSLxvMf-tUiq$`sg0pSD8RUq*>m>d3H-0so$wAzK>&m%R>+k;R>W z$U^v*-jJk=1V6rVs7~*`f^xSSKMgLjT{*b{5hMj#Jio^NNCS)5zphloQlz`}uj;Y& zWNW8(%`RVLw55IekgAvCEN}8Vqup1!v3>M+C7g(G%b{kRX_`~Dg|3U~cIJNpoi_%;Ge zN6}(q{CnTTfzwhM&6<`RWHT=rKNt^^g)r^CeLxu4#%`<8&HHO})*nAi{!HW9%saE4 zMpz;aAHC;T9iPYjW~)WHhCsu0y`0et%pyyDztHhzK4vST5}{07$hDutgPl^+Nxbdb zuY8DdYzv^Xnz~yjLIQ_GJMj=Zv1tI~=Gw(mWpTu0og4G~`ycs7yOAi7q@=?-cSOQ- zOBz0fA|-_|odr{Vu*c0Q#PzdkZDeuU6S@-_(rtp*;&m1Q)geP4m70WbXY!cp*XqjF9d# zDdj^c_Xwl#-#WJ!hKUGj%E83U0cj+R8Q>TNm*C_iC7`&I5c%BViF6?S^~B21WmD)= zn~K8j%x7dN@#Bnnmji_k&JI~{ypU=%4iu0+aQwePlEwc>U@xpNn=zo!$$CtFXEJ74 zd{*uKNb;P02)qJM&)!V;jDPEX)DGKYsBi`xpEfh=jlptH97G0>=Dm*JTS=yRm(S0w zl&&mO9ahh2^C3D9ZewA2@#MG>5~Ix#)GjsRM0T2PKHu7B9C134lOS zGIDZNvCW%uTP+e{6Vx-J*dCXj!)b>(eX%5~{UlqBMs?eagBFd(It+sZeBn4PlBGOi zYwQk+;}$JUOUN_!P=PKu79|OGzvJS+mBy25u@(r;oqsu;c{WIDe(l(+ueo->&Y>{( zJQzJOY>?5cDm+J&{m|4-a$nMR7j~M`Ycxe0;i^SQrhX#T@4-d&jw+2C zSXsYByVq@eXIE3ZJbWk~K7=CM<{nWV-z&<<*_MezVH~aF3GeEbdjXG&`u)04K6f;z_XVV8wd-hn-mcbKe!k4}R zPd-@O+5rWHAW5`uRpUK~5S`JG2*7p*3{HO+oDS){ktPHc2Vn>YPdCDd{c2eRJLF1O z7PLO>m_mdmU4)DwnoeJm5_gM}d?+Fu$Q@7a?@6kZ1n{G-ZXcYeqLilUJBJaP{f6}L z{r)kG>&U`i{i=D?Be~6bK4$K9*PT7m`RV2R?%F%*b6+^OO8nXW%7gbU-f&QS z&!6+>{`+sa1)zBL55GG2{vZCW?AqJrjS4uIzwoOcZy)lxZ(KB#fB*Tf zy3e>c@b=>?_UPK!&-~OCkE27ascOvaQ@QMdhNw zQ*QXeys~tuu5-t-ho4$m-`wF`c*BjCRR>sCM_*tsGIBayw`5Zbmt6-=nt$UJ)19gy zWAEdC{M+i?+!8l%@+{TpZEUx{a{qmcH;Q+p1#`aqKW7h8J9aF4^vRVwT9S{e35s_6 z*{>pJUL1VqiRHWFzHtlYbU*XF*BZ3wtRMc*IevDxNA6p^M%>L;b-|6FJFVI$7VVBD zkNhQh{DvE64Pw=dF8t*$TTYsR)-KuL)_>lOSIiLg*WZ8P!DqZSDIPNQ`p;jI(I#ws znfu=P%P$|UI}m5*44yIn`uUSJPjAKBd86CTH+=rG8t#rBUHEfef2T+MW3T=3HDmah z4|+W^V9NDhyljxx>k{QyoD!8dncX64VrQd6$ZNL5DpM7V3^`<%1&XujJ|NgVb z4_)))Z;cIRR@pF&o>-6_{7|!{Z{QuZg)f|C2}gEYm&S70?=jjkF?o_)e(=!6^1qy8Zoy;BHf`-zNz=OY`@M{X}c52J{f|N zP)U)LwF^^^@3g1RsITmyq79UU&OiojNELkl4#2WOky>6nV8J|qi@pxI7yz5{8ZmnhO$ltoRVQ4V# zok10*gfUS>EQMWE zqt)NuWOcOrn%fA1P)T_I$dg+ON}6#PIJJ@~qBoGIhfXE8a~L9BSpcU^9(R2q4sVM$ z5da+tUw<^Lj=0Akk}`h6|9;^l8fn|KbZL0>+1Jjm+_vnAH($E%&;FZlI)&{H5bKCH zv~-Eu>x+MR`z!m=;OQ4mDmOO2yR&~`AUthW#g3Qu7fqZuy9W7dG;(OaW#ySx;_V{s zc4KGUMUiIvyH7m``6JJ{phgasu^Ybfn>!ZoRwtZu)!6(!&pol?55G{p_k)YI?F%1W zwv|2Q)DfYUo$KD)9SHSUg4?!jJRnzHaOK3j$hs$=U-id_2mSo>llhB8+c&1vN^r|3J)JV_(sPd1ns-t=i|^ZOmDhPaR;q z{_HagfBz%$?Vp}rZvN|+KVG^YjXv}8X#>Uadwxv6bH}VwQU9fj^9Rqo_L5o1y7!%hOW%6pZ^e~goK?#yP$g7to}P*Iw*z*^;N5W^_1EP#S+A~^Vb1H5 zRl(ymS^CB}%iUOP!t+vaXeL|}8M0&e&SL+kvK9^n2S+09x?M(Tqmx_Ka(w0-zh&a$ zFq?8lIv!UcPVi|wdQQRt1S-a=O-w>K!sFh3LcPY$w@d-=Ori60Kp1xIGOlopxyv0} zIGYct4F~q=CP*q@K=Xp2Y0w=9L)U|Wu+LYZD1Il&0r$lbD=ir?-sno z%_XjF^5{|!pWIkb#_;`;p;xw1NSJy+P(D636OxJKpoal8O{tbpfh6`z>{)LULKJ%$ zAKT)I?ccs_&(fEbz4g^&YllpjKxDt!(%u!!lR#1gJWt(sz3_{~ZPVJy*(P0|x2)Q7 z=zH%Z2Tv02%qdonru|ZTGZY4;;u>DHhWfW3eqidjv1s5hKK}k2J>F?124OOm;L1^C zC_H(>xXNNUZ88M%x88eKKjbT#ei{vixEz7{G|g6K>OA3DL9Zr;z(B^ zm-ZCu)W-vYyxj=Lf3ER>(oAJ=-wbef$w|7ZvZ_n2`(YyYcHjAb978cN)LeMmXHR2a zYex-7Ke_eISKo-7a!FA1s^YL^?OV=}O4ZzNed%m&Xqz%aC^{#cI_RG-wGNsvWy&c2 zih2{I2l;`woVvpyJoWoOxGZ1bZqm49@oqfg%3H7Eo>&ux2RgoY|2v!8FBuo(Dw_dg zCyg2EM`I?Qo~^#s=X~Y%bDXcW>fX%{uHDuSlM7gD8VHr0`@JupCGi&-*!ta{Y;0@= zD7PAxz1e|ATzd0WQ;Pt`jH>b9a>ujO%xt#o?+8$H?pJR=H|=p9Fxhzv8lHOFZSETZ z6xBTYQrn=Dr%WEnNx1ecVRsvH{>|5&&cn<{j;;9Zx9?fJXx*uETNt(*bpFksO&Q*~)K-(?1Z*c1GwMp%;0F0S9E87^eOax`;{_xArwfx;T z>}-Tt<#yBZMZ3|61-D+EZ0A*V9rLFM|2G<*eA{i8JN37kSdX9)SKNB_q-2k5>6kY) z{|MuYG%n{Crgvo>yKus(ga7$b$DoN*ri^r2^8D*AXJ4Y?bq3b<`Z~`04s_nPmY?8t z#FrBtq3qrKz}l@HFlqXElNR0g-W%JD3x}!D`u-aoc+mN$7aX@GxG&%x=2lT*wRCWu zC5*I`Yx8W&FtBZ@0d;s@UNoxjGD#yRopLhIijW9P1WDL}YSzBnVX(xr5o3v~{SqO%JlBN`&CRg-9pgYN6!wGsLJ0Ul zQ@QJ&2zWD*B>Q}&sun_6;wmHL`qL%3OcQH8$_AwRPNa3yx{2p0RPcbX$-r%W7`d^7 zZ1-cUEw^FGp283JEqb6?p&rHA44^$UA0m}!(u7LWb(0d?;=*7?8+Q2@K074ZHFWy) zy{DbIQ&k%kMH(>3M2f_>HEg>s?XLMNHR{6I_JlP*xgRH0cQoBxMFEaaFuaIwlvqbd zV7J%TcRlgGt{5u|#@ECJA8!BMwq+ z6w{)S$i@%k<^Ss5xve_3uUb(HLSePE0>VWQDyI7yc5Z!NkF;ra+*WLi36E29xrUNx z5~sk*3>7s%k}xF&bUdCU@HApHAOfVkplxOv5xy%hFjfNFJtx1OBrjTwAu+qz8$`Lm!CX z8P+X9bZ3KZ^6xQ=)17oifLpm}385+_ip|x5M99Fh$ZzJQsTG!4Qy6)7PknoSE30ND8T0NLl4wz2gk23jeoy~Vk2Ym?wMgC zKrIFE6 zPE~dE1Q&f4+@3pC`v7@YdUn_y2J)6TmLNf^s%*SxRpj+NZT5iOEG> z*n|+!Oog-v(eCl^M2v-rh=l2+t$Po>xiqrkg@YC4HM36bF08;MrL6@;X1KtT}N8+i_rxK_9;8-puAG57zlY~yc@TyJ+S1p^0DLL)H7YL=MV4fOFwd4f2OEL zz2F0yT(2Ee95+!~XK$xBOsA(;XpnR855gvf`yv29-+5 zSOEsZu#@^ZWBvEqjo{F}%dM9br#0Kq z&;5kI?@LP;FMV>ytFO$w`MVbvdL1$Ibf@3{vZBdxXzf4e^!|0PE#GC2UiC&J)XcBd zj$_n&UlOAb5m+ItvcwXF!8^m&4KuEr#55#o1$@4Kgy@m3gK?9Gwb~+UMbzY-Y*->@ zR)>}jh(zE}I~%<094;v{d}xUT7o3iY=PcvcE;ajwOK3;AUJwfxXiMgtd5XsK)ixgN z+Oi331M&h842Q7N9XAC!6w4}GMSU8ZL4HYM;yvcZ7p@>fspB?H$fNWl2QeIBnN15i z5i7fG(Y-5{`4BFfFrLFZ&d7x`3`u>o5vIK@N?%fOa`S_%-gxOIr_A3Z2Fzp8S0BEg z$$8V}-Pl%Bv(@kG;&BwnXDK$d1hFhiki;g{7}Qn8;bc1YWJ(O0%o~uVzynBTv(28k z{Ue+GHm+QG@aI20sivl4{-yBrW8m|lt#wO(dFN>d_V}lt&2_~+ceDTWq!}~?4<+MI zJ#Jn$Z!yLZ)7V3 z6qy`s+OmAv{DI|g)z#3yJdO*xY|<_%Es|u(iXIGHs`S^xY$ zUw`h!&y-cT)jXy(uuu9n){!8$vm5qxI*8OUw>VuaDFJrYrX3wqdgyWzai|nb)|M@M z+uENLJt)5E`|iQ_g0e{jc8r%YVJs5V@pZ3dT6NVH5lJ zl3Tt$BgjwQ`)npnPbzG69AFfmpH+VIj_AqaS=8LVF`7k?miarzGv2Xr<0e+C`QQE8 z3{i)@&v`>a09(=nYbU~2wW9t2@4dURv(~vAI{5aQIISLBQlzuvYc_U{o#Z^_;9F~S zS~I91-HL@g?XGucw@vmfz5DJ(Yckp-8|e3(A_S6^t0vDF7s!wbGPjamdZbqdV{e|x z;t@*Cq-ecR<~uf zMPp5oXsd2^#p2LyagAMj8+5eud=+sW!VI*<^5sv>mFAt7}s zM=im1EJ>U--!B0+LDD!AG6N5#WjuXto&x1+bYOq(nWw1`z`#?ODFsN%W0H%kC|H)5 zbRtRomY63mr8t#O0lR*EKxo?T#TR0fI@piF;=qR(W=Kaw1vhMk0|%h18^(^M1%>{0 zqw}%9SKs$@m~#OPEQl8r3ZHJ|1P&b8^M)RI=X3uMi6K&m#~ha+F9~}3y?WDdq-^S$ zn!Etmo7cZLaObu;U;4(9sb_XF>`zg?jbwy0ktv;W-K#j6_b|dVZ4E>z&<&@{WXb(f z%v9_t4!!he47Ll%?&tt1I!2V>K}GhaI#QIK+wlW`7*(!S-tuKrH`Y*lYC!=!c<0ES zZ|i;yz23#ba{KMLH@G$j)M%MiH(nS{loV~EZ3uCSjppEr-VwmFF9 zicz(NpZ{v%h(R!Ta2HmEvCO3$iVNy;CE=^kr&eO<8HSyL`cq@Evw0vOA_Z(DmBgZz zwhq2kF;P;HsFd-@RQHC2oPu4o zhAYyf40R+}mr)|7y$^(iv|DtqdhzM-t^a%Ru#v%wFW9$Y5&ZqvQtb$Me&OJU@2(s= z0R|OEgn2vABL~OFGpuDM8t#1lp6BzfIK6Vm(nnv}kIK)S8uTh)ib`lhW{vl+So_#- zo`(ff`_uY0o7G8kCYDP9KaFjAef_?|V#!xgHmcSSt5-g^YWTb%{+5^Sf2ajes$+g? zjG|U`_Kf_MZ$9|rCoZ`9lu?D9`#xB+O}pr_Q#B_2`+ z{tItDcGp65<7w4o#gl)n12p~2aZ;(lk3TMs*Eq*HC~-(Z%ycIK*C@H`<-fl!%@l32 zy|Ghh1+$DelcqpJLHdxA3t+!G+o&Q9xh%sRr+J)*#b*+ZUM_pG9FW#|kCe2PK>vSHJB99xFj-4$=?hK^32h=O6L zfFJV0JP)j8ak_TPGF3s^P6>8{Z3Q0+NjIT_LzpiK{~d9VXSo2Vtf(44VQ*=p+`oU= z=aWLAMk>N`+5OqSni}@0c?y*gx6CS%q_7$|av}z^h&Bh8IQZb+mJMr_ zmtQi26-`UX$uDRs~`?n9Scx_b-46aViQSI)ie%x(9+_T=yHVkh{^N0!TV(S&Y~$)bVO%`q_lf4^z| z@sF!tcwqGlV&fbzd%i$8b!%U)Tf4{^8WS)4Vr{x!Cp`L$iN$qmUbt@)yUI6Y`jq}} zt)sH{h+xViR3*_c{jRxr!SC;Ubm1R=v5-@KjI9c6=ump@)CE`Fa=rQ62bVv3?{e=Q zC!E9{x7B@|f5?m}{ohX3U-O#-pM&(1hFWTrmHCzY_c1AD2J+J8?Jv zluN!fpM#IUCC>53lE>$r6rd!+2B_EC{C2GVp7VpbW$nvboHhY@!ugqP;#4PbEgQ^= zLOuCBc*+0pc3zd<&W>!K(IfM2{-%|F<~#;24{mD*7(cG6=g4uNh=OicE_`71!jwli z4U!lhcP_|tr+-QpyRVZmgww{rgbTjF-WQ#8#`xm8wW1@C*AcQiHKl2DColZN+eMR4 zDLdYab5~c_5l=cIQ8KlNMM&J}wyc<-L*XuX4ASXwJ#MweV$qJyrtYrxXslJYV(cAt ztJ|U+_UU?rmCi^yH+{Y7ho2=gl`9X%>ia;6HuxH1C*4>2#5DNOE(HKLJvIIdNPvthO z{1*cT3|Gl;EXxvB*#}`PBS#;H67Cp|kO7&1(phsI_W^J9!XFMf5J8N~hF)h|JKXpG zHZOjD=D-@5FcIEn*^dr(1ebm>`A`kDT}0yBcXc*-(P8D==-aUwG;r^m@kTH$FjsH1kz8usX@J_ye@OYZ*ut(TlP_onNw zPE*~XqwXZc1P@bHlU^_ppuv56cmDQw14a*1KJ}>`Kl=eZcmI&QAbj>q@cOcyhCb+y zdtlT|pi%DBjRKC^3jJqN9Se>~)|fikS+`Mr^F7J0_Mbfo>UZqE|9-u0!?17s4_taf zmc8BPP83`pRlleCz`^pN!=WVK!h9|`DnAMOj-3#}Yp<@j>Wer1?%sQf29L39O9)h{ zKC3Ry%S!2+IA~JBAX>=ku0F7{^v#!juPu)q*tM@}RB*zCj;YhThm6((d0jNb)z4Iy zsl-`x_;-QU1k@whWaHi*ml*9{#tV^d7SLb+`1}3=`S$PM7EqPLYASng7~)hX`54Qz zJ6?W)9^BXO>^Zbw5t!ZR=5N5#fAVgK0gN0DKmIkHIGv~kOzu;97`Bh*(XnX!fd}rt z<}=qEV`tLtCvN+{jiYY7{mPM%Xa`84peFT{BokISw!Op|tO^Fz1k+ie#nR$aosj-+ceFX0&*ncx2z&2=LknwM5x3H&*~^5XW1&lqiZeU@98YnlB+18 zf)a)2M53)k-w_Tqb{itmg&xBK2Qdg3P|FJZlLP*t3hs{;pNvFc9L6F)k;9+RV+=cl zxQSJfBnViJo#hw{I}~ndZhq>Sj<;9MJpU57;yQ@5!shqX#s=8mz>uqQ&P8zTw`u<& z43>JS#sQ_s@eY^~l)5~ZAodn+&M{3%HU{lXBO&Xo{d@O*<6GYvKYrZx*Iz$kowQKw{4HSvZVX|KMfjK-v5mA`i~g7wy-#E zv;9?~4k3fc5kgRsMIr&<+zr@M+PM!sEFHO%oInI;9TNuTyg&4rr=Rg_+F57K4&`%c zhe>F(q`d00H?()P7wcB^P%|Dr5$0V8qsQ!c>hY3sQ=zC_B(cyOYd#xUmVkHYKP#Js zCr#8Y_2Q+ z@;9L0#|6sJ1>|gqB$IQZZR1$19Fmo;xyNS zG?0grmvG4=qNsZ-hjf1GGx=jr*|}ju-utibe&e;GcUP8-Jh^7#w9eY8ZHlyoy9dcc zk~t+JUnf*ZpRk!NnWB@%>3qvB4RXQ(U2Gq+A_rCFm^<$6w^!}hv3>5`xh19jKoG9T zl2kl>Dz8FOF;9(gpmSeSYei_b0nLU(2Wt4hD}vT&DFvc1LzFWfcqNA5DAoe8&&* zIMEuZc&q0NW<8#kx+#0*du@}Wajrf3$7P&p8cNly^DB?yiP_&&(z2m&=}29i{e-Od zBAx3WczPQYo^^7+<34HnC~`K@2bjl*u~mrS0ZAgVEweR9w$wl%ATodorJNB}B$WX) z%utBL{)gGJ;tVFx5g0RNjDqL4Hg<(eS9A|L{KM8Ye z$HOfcu#?@@&<2N%ca(vT$ByUZ_~Y0gNOk-5C1~0ZOwC zB^+H)7LKVTPD019HM)_ZHZiuYdgp^zUY$R68eBXd$_J76_UzbQKa6m>Ss7UcS6vHL zLl|I=bGeydF9IT?p0QZ`d*8ct&z`*u+B+3kU9h(#y-wKR!G5LTPul>& z7pYoM=X;C01JV>I-~c$1lAAc8vUG#EXy#s>F~1E%fAg(3D@zL#At}_&N#$;(P8=o4 zmJ*an6oz3u_0*Fqm#>&TXU?_PUVF+Zrv&|e#Bj-o5k+jkMkBT7T?{@76Gy|g?YlcW zhYY9ze}D;ennh%xwxQ62C-y(YI7=NVsCnm(x@Z3}GaP_9^NdFyIJo#9;TQy}22@^p z8I+VJP?h6lz&r~FCBd9sZv#iDY%Rfk0wDREgMW)mF;ZocWzV=O-?8Wo^ttz)4G$b z+qS;=(h?QRf(A6X6tLe?M{*(*iHL4;7CE+nJoD^7AA0Da$tO?x+~>bAXZGxZqC$zi zTTu}Ln=LthHckBD%1KjUa0N7MlF1ere=_71*%(Vb+&Vs#9{eNjX;ut4<;P(LWbawR zJ}k&M{Fz6*!!ZO29pxoQ`Hn|<`7ys>pIp+1J;KVc3%~L8zU#_k>*8LoGS>g#)`vuL zi~M;v%+-$n;@p>!vm*8fQ^{HGd8z;+A%*4x*D89UCEN0ndHs{8(O*>*( zhNbH~)flDhn_+YniU=icbZWi9^eABJuIfdAGH;Le9( z{Dg>VYhe3$CSKWg?N&tCXK_L;V%zq{jT`Igwn`E{f+jqZI)noy*6Pt{l>J#(SGQ`_ zDqlf9M_YoXI@NlWkbKiDp-16_=i@L3ZTFRe-(Eenv~=PG+;vhFjMh6oQ)j6uWR4xezGxWrEiHKURC)f4+?)}4~58fwnuYX8j-<}Q9 z)9%1tpQ}Z=WTHRz_@mE1`|RyMz5UByyixJzF?qq1vBcz$%dXau90L@1ZB@P6?~Jiy zAf(7(Qb}f@RyHFLiz;RmLPcERjQ#&f0MB437Oc*Fb?WG$u;5B)ZhG^{7f+Au-c?aq z^QG^=%=2SFHCq4!giRxrttFteLX%2bf4dJ;bo?P_w^N5FkAuEN>Eum@mVqH87 z3wF02_Kme(JRWDb@U<1o*ypCt-SqRH|GcoM(D8%!bEkg^`WL}>eh6#dGj5u#jynTp zUkrJL*n@QxrO)}JLwFxKiKFlI8?QU1WH{tXuJkFm{5Xm>;tOBAq18|-1`jRPK8^?W zS;Cb)XkL56?inXm@AJhC)8J9H)G~|!19qmtt+6&?YZUuQ zw}@fc4Cuw#G1KD7ySqDVjR&?WJRhv13*jH)`Cf5HIwJT`c)-5nn1)L zD9nehPWbEZcP?8vh@n*llrt}blP5BW!Cp~jtr3aKN+GBc!+5qU-V@J7NeTtD=bwFs zr%3kzkew1znv9S@ZniI)(31gGRJ>vRhMPYB`I3^78*aFM&YW}p_Q*rq8|qWF7r58% zrzDLqB}>5`H?ky@5Ilh$Mui3W+JxG?H(uMj_h6CXq_e|r6u(D?b9imO=3k z*&dA&>?z!>IQZF{Zkm1C43UoARn#4&DuKx=4D~+x=p*aaub(_=(q*5%Y~H+!Rn;eU zeJY8~l6}<>(Lic#Y|Qujp|XaxC>xSY2;szxE0%n)a`~vBA6@xbC@BN&KMCGB6Acd- zHD>UD0aOV@o_O5azlULjnzPS?v(B{=gHYF@vy-!s2SX?j5MfUetlzNlr59&UnhB#u z!nUo@9)*6TFnFlr1JT~m`NH!H4<2gjsh&$L z9L~s%HcDYXc7#>$FI85U9Ju)9m;UjOe~cJ0^3(I?&Aar|`2__jG9Lza3AAJNn-6vB z)#D~YzkW#$ffRujdCVIh&seJ*S)0Q#xpG1ls#OjdRrxVK@T0>!Cww1dzAwv^2bK{m zbN4SBSs1GzS*mQAW>m6OTk)x~?N>~L$6s*OoT*tNxdrD>G3{>CGB{hF5oJeBRcGKX z7K`b6Al5-UWjhpN5QHstEyQMvrO&pzg|C*dw@O?Uz*aC>m@S2H3)u6>k%h;Tg!3?F zdrcfjalls=${$nG?!s9bn8NI^?76lz*^o7XLV`Q$)a6X!8#(4e2({*!Uz##Rm(DE7>xr%=x*iWK!m znu#{)obClzIKH7oq^`9X>JdNdnCgV?Y;7H}>NSPN1`Hk>lYIOPtca%=BCY}@)EFr= zu-QhTLu=ea8-pBIDNgVtpL)uv3$D8I*haZ+5m}Z@(>izV+{&r}ZbJ~(^0vj*A~;Tv z+_)DtHFRv-Hng}D%7=sG=Z(XNQEUT@o5Uo~?U{O}&od^$*j1`oOSPk&*{LeI8Iz!$dEpdMp_X972^ zzx?G-Z@sl@=FFK_UAbV&^l9&|UQM~P0rxf#DO^+i@OqU=QaXuK4cpu?mk0mz0DDR_ zgJ;gs^k#@eQ7qcj)za0tf8_3c`Kl_(RzR{4GHqmI{y-*=Nz~yUN(t9>m>LnKC|h_w zxbagIC5+NM9W2AHXwrm}Z@liBWA3%$($cYGYR{fKXY`oSnWG(KHcw4thPoSfzq>44 zJ_sgE;rV}(S(_=f;*qxI5chE@J$j-c3h+@V#u57O%k7sd|CP!`RaMO%P6+Ja#|-Zr zk2GS?rl{3vSvp%i*^0}v&k{j!3_L?DLF{7@$+Y-)0x~BfCOlmPU-YnTvXd2_Yt}%P zNhO2J#5Re?beV7lVw0L3T@j0?aa3h4Kg&EXC1=*-`@i_g)p`kg0YWOq4C!e=4M3J8 z>5^3V|Jl0^z$%KoKeOfa6cS1(Ata#+q1RADM-c@)#R>-~*zP+U*gMu!JOxEXiVBEb zKw1)-6a|3*0SOR#hXARs?#?$eTi(8s5J=2X=6767w#?4V&d&ViUwOU;$5{lygrMDO zjfstFY_~;1==Oj!L_Q;vXvT=h06Cg}LycqsRdXq*O4{`8KY)^wsh-D^yhxS0oTB%; z1ZE5*Q6Z?c>(;DOhfL=`fA!Z_pV$zHy`hcM%RQF zYore}`lm=llak;p2D$sdvPFA#?rz*52|V%`a76e_#8{04l0$*&QxHPaWG$8(lB&hT z+<4PX8f{er*D0Bm>-Lksg|rqgp1!TP0tUygcj9?I+f(*tvgq z+wKF7_3VGaK}Zh`x4`mbtLuH^OD0 z$Ae?&&|x;4O%O?38sfYH6te){EW61tTfT7r@@1yeXU-nTY1yG8h=?Ep*OEf;?N>*B z{4TZc0C4m$SUe9L$^l(FpwPe{S8uFi)j*7XCkGE4`T6^{dFLW50Hh59&06CR(?s`O z_*ES*E-tZ00B>Q=H?x}{9~e0n96AivFR6>~T)F5xo*C-I*YG*Pj?KvBf+iy1jBr>w zb#8x|6_YU`F+DUMHE-U$T6JQjE|^3_U4;z8GT?&%W$lJka)rNrx7)`b*>BA|RWJXH zboN;Bg>x`3&y{~ca=Fg=$P~9frgq(dLkilqJ8FycK+Yt&43-8OQ`E;MHGl~EA_L`! zLA+KWw#Y*alqCxukaD@V2Zn1>yL9Q^t*gmw3K)BeD3r1|3$9MiPPBP#WXGN$F^RZm zLd{89hVET|t@~x+fV4iKb4@6ef3*ONp)oLA6xFI#JAL|e=)o$Zcm7$y2fRm5PIVwAmiReZm_IA`p#6$TUCVxtpX`03iPrN*Ft@TJ8Pcqlah*+vn zG9eN<5-hY8d84a|F%GvVk_fRRg(ICZd0wlHtf(OXPi&hw9#~{3&p{PCCub;0fTShII!|NL#uNzWZfY7teh ze$%0MfhLUx+;jJ_r9Xf@JBgX00K6WTymG~Fi+*XgCp$s{c?S=~-u+Jy5sB2jLO^AT z3X4~)$UJuJ_@F_98Z<~yU!dx(060zPh~FC~$MlF6a1v^Xl0ZHFzC5Q#^E98@vFY@zX-T5hp47PbP>#8J zEU?&syXer?Ekz4{N^RO0bm-z-v%-qsa!fRkJrG0A7}ZlCp$V(S{2|r^ZZ?sJ!spo;`WlTg;~q+MQ?3 zlDj(MBTN=+Yz+_>2V!GD)Aso1q3Gx$(c}ZrL_Twxmpky8n&vlnkHm$OOdAI#N4?m{ zgr!i`%Sv2hwOPW9dRjYb{Rm0anyk(Cf-{azsUW7hk_}u5M}S=3J)1V2%H9RK_0e7B ztDd%%k!{4lSX(g6W-gx(2)iN`-kdIf>x6bAwc z_JZ&~A}q~2?}?0!qzS4;I{B7ku2UB-fi_jblfN0Dq60hk?bv4Gt!E#7p~g+Oc>8$k z#wHbR&uZParMcfA(5N{OECn^K2W!=INzNp1J}h)u7W{I0%E!GUY_P5kTqpw3)j?#0 z5}=%x?w|N&2`yW;^wCEjJ#q5H&|8PjpFdv=j;f$JMrhJE2n}f*4N1T_gDH|81~$m{ zPBzS6N;nGK$yV8eo7rjaAGo=XQ(#r3!eaO zHDf@7UfZ{BYWe#gfVT({5vX+=OpS>%W>j@hcnbXVtsQ@S5mqo~X4L9sr@TOPa0L&& z(qP!FVDrY)XD-z2-Wx<&flOjS`iOn4ER;S?Nq9FX@XCNyn4Bh?QzNDLbB!pT5s*ec zf?=3pkOO#7h#x&>iWka)T}({tWp34?c1bbhJowpeQcvJkU#?tpv3;WY6Oaks9@FOpy3l5BdXxMkpj1APHOL`HFHYL$Dx8dya#!JZO6C@4Ufm6gV;usBB$I(uRk418zE~s|fBWo<*{$z?ZA8;6 zimP2BEj#(+`;*rnCw(8+|IsJ=CupywWc|0_<~18KI3?zasVYMiAu=+uL4yWo&z{Yv z2E;KjF__yq9F9xMpDt8rR!}ET8exD$w<8m*K$)Df`;#ccBC% zNRUjPI25Xi2}$KVRFNwqmDG-S7cB5R$y32&a!A9xQ8g^CDX+MU zD1T9cO1J~3rgH#L;WV6PgU0ESrgC&1GzQ9}JWO&#MJ1(nZ`7bZxN$Jx&0>w1YGcP& zD=YwZ6E()?$wHD}=qoCW5zWw543_-5;m2=M+q4398(1^P7nvZ88jhl(Ab_Es` zLwXOgbm|7`)VD`Ok|b0lurW}*9`(qOL9;pSUpaUI_IuHm6&uA6!9fizd$H`MF#LW- zQC5@P2VU<^A31#LjaOW!Pk@IW1T9)$Si6A;tn0>O8sOWtBX3Vmqu%|%#*Kge^nDB- z-VxPFG8Qc%$~BaGR-Fo}w`)JN&PxCk-7x>#wb?mxL2+W2&hf2VS z5A)~EvqwbWO-rZF9m7aOQ9x=>)yp2D)UXs~nl4yv5-!oU#%-)fdHjvD}P>r!(p3#gdEVa(x zot}|*!|<+k^<4$@`mUTWfmW~i@UC^rf6JkzU#&k5Q}2AVldS+KcD>59=03Q1W41@? zT

quWIJG=EG9!0EWM5T7(c)V8mmF7Znxd_jnIUs{6OnJO9i_#Vwq~kr_(j2z8Tz zUrMLgmT+`-RCFVV5+$IynL>RBiKy=>4O%Z#b%o?B@%dat+shMw4X+ou3oRvX?|GZk z;Q&?}Nnd8MfGCnE7SNz|Np)^Yd-ZXZk3nD!j*wwj)(#HAJlf%5<0hguXOliwOi|g&A%0WIXTs429fc;Rr7^B zpU}4(=-N#(+1)g}ffDy$FsNPn_U-%e$5T(9JlVTSC1rQxrWdT$3%Cdia^eEah-m74B>60fcjhw`YY^S}7d)ir zfrS^Fop}y?OH+2)zr{c?T-r_1Q)j@&@WW zHH|+CJF4lX03kD#AQuSyd6CbtSwxx4zCLQUfv*>l3Gep=*#!|EBQD$(GTt`IR${6P zz1nOUU}HxDI?~vUT!>|d9K!H4UoJ>|JODrtRni*-YA|@I`iu& zgExK=TbRyzl~| zQI`>_OluBhBaExMzFHkj@2++o_Z5rEeQTG23eY>h*F9e71rB&QDw)Hige6w@_fX9pTh9LD6;_KBw+)dKc_7*~0T?IfYZ0B$ zNtc~Fb8Puyx=9S zh$XTXqJ-MCVV395vuzJbl`HxmDL)<fCu zT(Kag#~?8Dc3_JF=g*psosNjI1I)PrS0speZcovln;N%H1&=*l@XHUU(-%dp_^n2* z1X$PbVy^!AQ&&17>by=`6cq_tw4j8AnBt$`vBmt}q~h&cqaesnOo+PWb})P-h^rCZ zpy9c%X82aEbe}$3h`cup8wUp64(zoN@t)@VoIYF^IUJ6gZys{$#3@IlW59rc!LJ{M zNWFkR=$<-$l)NDyKf10vlGfKj%lO!>8r%8Oo6$4ns z;|kIb8A2m(ll`VlePfhm%hGMxwr$()t}ffQZQHhOS9RI8ZQHhAoqNyy#*4pu>|A^2 z%8ZyZGG_!3VSw`d7S$dv)m(BO+Fb^8O9c{0Zy`ktvLPJ@pNsQsHSBFPgf1}XBDM;) zH@N8vl#C;tZ;5mb{WZ(WP4{E74InHwe)7QxZHC3h?h);81M2TjsmE;!6rPhiOi zkjhu(oZFvG-?q4+A0;#Fd(&F+HUr`JHYgY6Ior3<6?IeMPHK!!%w0+Y+me_!%;yHmYFXHLo3b( zIj!@hMs(U=gY6>eTi*m?(wFWnkv)k_s`ECP(3I%zW=%lY(+ z#z2H@W%K;_7+B;0!4+6ZH$ZvmNujPn0>6HsIAkAXg+6Cco0ucfUW21Ljtr>BLks^c zDrziHiyWt{S?K9ibT9EAaDT*)1cnzA93>O2ADWv%mMdh@o|bC2IA@t;W7uORd=Ixb5oU^Lc33Y`O6 z!RrQFO$LTq4j}JI8aXb?HS)bABwXN%L7as{3WP9{kP;#IL|emMT#QHEa;C#?Q3++R z##|)tw9MWo>1F>s0YSJx!txq|Vo>_Ku@PIBRvv6`3Hz+ttxuFv?N1tQYjMr3{SvI@ z3Rp{`qrGV9EL`E|go$BFTEY6GIN4B1Q;Jy zh+e+ZEd)PZocom-^M1Lb7zfx&H>2(NZ}fi}7ein%wNfl9^-Ycj)6;Em&`o>Hubuz$ zSgvxA^6JjJ_Z-N@_9Mpf*e-{@?^s1UMKe|!je`+X@gsbgH_%8q%m{yKzfjd+JNL5RH$KRR4p?@`qXwf2cn;`v15H+~71K$+&S1j%+ z^Ev61>I9qD0Vo-{6kn0#F@q(o!C90z8Y|*}e)%bS*0M~kCaT}wy(Y}%+l1h3LT39Q zF4Vx83tygW)GvBH2lO^)PHB37^&sC}ce7i}!VWDk5T6`=1j@-^Hs9B1ji=D37;e*d z)3~0ML9(^8ei&9l*5=xo;47yWooYMb3lvbcAqB(r?KJ-0cMi787bOFVN{E!eWdLC( zgXVouAa)Sh^Dn#Z?oOuc5l91&gy=ls;TM9u7UCOTqhO`)7Nfn zfe*G2v|9HK4XS4X;PYF5Q3NPxQ^d{>5A_Y~j~&=^f)Xup{ZXp6-X(ldA<{w6riSFV z8)&Y#0_ve>NW#jk>w?N8Qd-F9DJ|{3K zIshF+<;OOCnDtDx$lmn$6nWF-N}PC!Y%ThMo=A#6N7Yb4}!0WS%8UG`&IFMiib)`0eyCmkUkp>YZ@C)p&EYi>d3R@YxLI=)1O3 z8cUxyVU!Z+Cf%%DN^1$(PUYIo3yKaLo28}CW$1nC0olwAj>Dr400^`Ua_{UIKULm6 zcD5C)O<}aXoV|wD_Nx0WfuKIIPP%ezW|~9-a+0Z(25G5A;jGDk3tRx_K^E*8MhG2B zD~BEx@x$v7NQs;4vze5NC zT$NDt%Lk}5-ZGAYv;D6h*+^_EB`37F~CAq1C`h zXZw79Jm5-dzP+1CAsWt?-IGNt+^gP@ zfOG5!^BNkN%7@SG&`_lb2_ntE6o%k=L1=H}%IBoZ&e-lWXo+)7TCVU4+J5m$omH+- z39KUE&7B~jd=!CmxW5~43H5`4fqA3U$$e<*B~{8pPN}sVrQ{eyI1rGVOzIwfhojEhzUc!Qa$_@ui?x*PGA%>jqsN8nLrm)CD?YKi zl{e23B+KDr(~Z_LtMxM(9ZmF%je42yFWdx9c~@`SU4RR+AZQYVDwLtceyvz=-$sj& z^fe2s8R9`6_V*g-(kS@7GN4xg7H~LbjQrZ>1vt~PTv}AuP_=xPXJ|v~B@X*d;}Rqu zL7J-mxR@1GGhQ>Rz>HQcGvgoi+q;u^*HfJVv%AZy<8LV`ffB%ICW66+=zz7#!qhAf zBmu9KY0L&Rad*b-Wts}ZRq|i6B+xig1|Y&>@tu64L}Uta@G?$4WXLiN)xXKe?=nY8 z?2Vzb(=XT}U=_IPQNNLCj1wsCQnwHfX=bPI(owUvX+(-&y9pK9PGygs!)x)RjEz!l zQO$D1rKpF|<)2FyyRFd4!-YY+im1yk*Zp10kBbjQ*sZQG?h#DaCrSSxwT5hhxx>uf zK|t2MX$LGPayeP>f4v}+l9IwEBz#(;BsbZYJ?8m1VBt zKdkJS6k;!yoh{o^seD?tkb+QgilOEa$6PA#-$0P~br9nv6UIUC5ZM3ft$edc{y9W7opTmg#_EUe&x|v+^3_8NZxJ2LU6lOz8yX7d zQ;$Uc%Mau4EFCFK_B`^ZR%Dm%pRCl>l?F3~ibX6Haykw+8%ljM;X2XHAz&ukAgnFS zN<<)M#5M~nGyU$FImZ+N&Ixa@UfVR?0|v&<#TcPl^|HWAsLo|66E?eH-_pM)9ii zsp$o+mg1=D%a)StuafavTjB*l910P2H3}+(+=_3XCQWy~Q~zDKp0kkMZ(D>jmelcL zDio258tP&*+s3y@HlmySU~>`vvm7=J(`X*FjSxh^PefAVjm&*D#iWHTH8UgSe?s1# z$0+WXZa6LQ@RRR5Gfx%(6G>nTGlaa6>$cwN9*gkfJ>CY$_?(=0p<`#RKcAn5>jaAu zt^V@}A(V2tBhUGhViK=b3w=qLz8P2_18>|2ggV_}*+r6Hr~%}Lk(MYb0(0u~gw?Y) zc@o6DXQUsF8$jc^ejH=*<+^qBO*FC-*kMdSzWs{&Q6d%#qeTo#e$lH-9h3;8fPTmz zFd(CZMu2RZP}9MI_2tDw4_+b(?pG_^G89Ck%1++E#_2Gqne7qw1BK|s4_|U->+D2C z$5h~=&ObZPw8NXOLD-T-r&3T(qfzGlVX?$LwG|9gvZT13@e5EWk>+)OIJqSLv*CVD zi#~ZB)dyk*XRU9hC*CJ{B8ohNMUf@v?I*-i^_8o$>z|Dg6Ud{8jB&=;zp&VWw1OIy z5RgGAJ5Lox76hD(ECOe}xiGa05S9KbAxf2_q9*tWA?$p*R~(fEvw&tI$6{oHw;EL5j>NGmTf5Q4N-eL))8BPmF)^{;ZAnaQxwatS@I9R+ z8v2IbJa9W2s|g2mVoH0;UkemTES8o)O_B(p5TL*Y?%2W^dHJ2=TPgELmz#%OvEc57 z{x4vu`I+yB62q;;dz&%7saPy{30+| zPEd$}5q$pjNMyupra5bK%4O46~NFG9X?$1{1{Mw0^u zo7qZ&pdl~Yo9aL(IL9oYRCpC6aR4&DH`HSD!{KGVx&~p9kO}}e0AZOi>e4H)wsXX;{@ zv>VC7nft5E4HK2Rtc0*JLXON7E>JXyDrx=HD4mE%A!HlQy#droUqBw*knNR&xsh3M zBO>qjhL;$XR4EzYQi5<{B6lixPU*vuR(^@m)Ntjcg#*v|>Tmo>bzITw65iCs0ry zEBdtzVD(~OtVZ8ep}uzttAIMR+@rQ!2fb*jy|Mz4!#$yDXAz%r!qc@$8TM+Oq4LKt=E?`l>s4=B1r1UD3LAcQ32S~?gN4VwDJ7a{*{e#1xhLW;Zx2<>x%hYR}l*R3{?%kkXsavKX(W7)1 z>_M0u*riplRGIO4_CfgUXrW%U^HNRgG10s=+dra;!x;E*vD(lsJRVvZ>4s@S7^!%y zfSdj6wBBdG&B-FXX49q47w!iWcQ2G~FZn#4ecbmOMM%Soh45Wdc^C>1T%vx^FEn_DIFCPaLp7%2w5b40sYz>n+ApMK*$5Eg8h` zogIG}v_BhLKU^`~EseZot&=-0c`d#1ub-5K_iMrI-{7VWBj&CNUa*yd z8vb@iSXWUxRfp})R#*n$9x^+|Ldd!;XM~D-4bpcbRO{AiSxE?rzy+f5ZK~srX@?7< z_5v9nERDEEe(o!7qy@Z?VbV;3ucCA|MEU;lfgox`uj*kxT_n>;ZpbvnxCb4G@*D>^ zlfU#CGiZS5?_R!t(?W}(F$yV?1TH+#zi`D1CH`dNb59Ui6PQ_9AmZSN_N4t%fbxm{ z;E1?7!R+;mWI)Blm#7is?HBu8U|B_(cn~e+cgyJ{AsWRbBzG;=_@;8jRa&8c3u#}=Xu@vZOik$4gckFtVKMcGST~^7oP+H ziEBBFr+J^?p%)wAMgOkuRERc5c`$S*`N@R!%v%8caz-4BP^LhC^p_WIl7-jV>xKvO zK4qA=elyBMs|}J=JEMaW!!b$r1cH5;0M(GxJi54ysNP4q+!+D9**s=F@-j-&m5lMu zRVM1eHIhY$SDQfs~i^1Xysjd79mo79YQaDfN@0r__H4ADtBR`F}t48P;s zr9hCfNQvcy96UDZM30c1n(n1eZ0I)-%sV~HcFsX%+zVu?HUr}Aw8+YrL${jXuVZbGi-Y!to9Waa2p-GM)V(Bv1~S)PcJ z{pr0cpXXzcATU@YCi7S!s8MBU<)Ry)=k08*8vX&R>8@Sde)#62bvG`|1^5~3GnBF1 zAsAm)1<24s`;>psUf9lwx9uBD?P0mxd<4JztNcJ&9+fBr6m&S5WGchuyv$9?_Y$@Z zALnhEuQuE#4uNV`>)lky(5K6p2y23ZVTZ^-hD->+b-IS>lidaNlh zD*=Fj>MZwf!ClOLaE*oM1lg?Dk0rV>V})d~N<;e|qE}<-4CQfs@5`c;$s;II()-!Z zdQNVO@eF!WD{cE;q+L|WC~Is`>uzik7NCyY@tj62?0jeZw(OxC%t9U|3QFh6Y~K5= z_p8o{G`ZJmBbT^&liC$dx(>DYSxqr}bo5u>pypKC4!QNBX*`luYNQJ|hf*%d_CHi~ z&T-bw>#se3hs<~VLAyb}?K~HR%SDJU!9!%pnmPPP*R1Jyh3A~||zo6-jw{5>I zT-a#oM!_@tts(bHw-$RraEt_V`$HbW@o!ezG(*=ViguT7J4GjzT;Ct>n z&fYLn8-a~FA^KnWbIl7(px(#R}(25D)kcz-dBhg zzZG)tD}OJtyJk=b#K-kT#{9Amq)_D~nURb~NZ^@RK{@A|=qVkDZjs&55e-2^3QgoO z5GS`cKa(q61v>j`_a@EfX9%AIV*N&2_@MHygH;mZ2fMfl(~C$E%~{}i@=XEoJ^VWh zf6R=CiHeDddc>$_gD$xjhD^?v^HjTtxSP-TC3?glt#k3fIz7@RL>tb6Tjnk8)BR@~ z;zWlpD$T%YkuK?qa1*7+<_2?4Ud<_B7IsdItK+lvopBJIRZcv}gjUg#bv-2JFF~H# zk&AvJEDSEt8!U*^dAq2*G?7wq%B`U1K3CG#Ez^0&7}Bcd|RN@UIM? z^45#=+Z4bd!7;ya7tdrgb5V0Qx$DlK&!$v{`1~qgrLf7jc(EXoK~xAYRsc8nz!pmX z6vp=aVgVk}-&q|&ASTf4=&VEjR07iwz%eE;j`G$l{Jmuv!<6FWr>%@>v~u|*L6J#c zA-YeiXXz&-s?AA3-vt}i?dybE)WNvj~Z1qIfmPq@?E*ZVhKX zuPM6&WC$bSyU?m=%+FMT+G}!&NIX`0 zl-sFXcm8Q0`ts#$Hnc4kbVqdBDp z{2R*rLIC4pmC}VhX-FVIh zkwgrGI`m=wSal?Z=7~Ou`4bSF30Ax~*p}~R=~Ig?5GhLJ$Md)s>Br``>L()2KGdnu z8J-m9=jZQZH<7Tl0xm_$;VL(5wX?`f+<{xqR*PMxuhltOgi+*j4!X;y5G8hIp!lQF z8lsp6c9SYXpx0dlJ@-Lq;l@q*IKhc;xNu|)PN*EE5F^OhTUy;sW6;RK)i30oXRPlY zafdwy7%FdiCq|)_wxpF0nzw}xtl5%qQodF#leu|nr*Ro#KO?12E5;@js%wA%8QH6k ze%VFz#W%lJ##t*rjWSY$1PG4%0eI>Ea)VK z$6~4r@#%xXOD$AKGV!IWBAvu3=kb>_&arZg6^kHGDO$5IcM~>8B4M$FdU(QwMlL9Y zd#a5Kq>*apM@3UWE#?!C=Nl0!`=?g1AKWC zXD;lCkc6q963!oXw~u2?j4cW55D^nshixs#*AO~lZaWc>U~y{ae#qU%3oT%Q*$^Ay z*`lxWje!A;-S7kIG^;_Kg9ghg9OmfX5iX;@kRM2l6F$qN8MH?HHTCLM!#3g6ruM`FUqOj39O@$5Vr&=wTv#4 zt53g*XI4Zsiz7p|%dSkr0*2^6$g09g7tyeeu97ZL%qQ3mlE_|cjj->@^~pmTWVj3@ zgvy}KS|3d^08vTSk!Z;VU9P&+5(~50bZ>E?ALVp#tAPo!RBVx;ACN_XQemTz`fbTD z3!#Ar)e4hjPft}(fnaa6LM0|XfNIna-a4K+*zfA0d3NkE+Rtg(h7}AoDO42T1edU- zWSFI>n296v+X>N_FdcXnsZl>Jv@97Ll8>Z}jcz2j?*YhwgnaQvS_>4_49Ca{C-NXE z6jBz_UeFCEoIph`iDo*Zu_&QUQPu`|A_N3Nc#8@uj)e}Fz)p?;(v2KgKZYEF%#Y!h zA5>rIIOuIR!)-cN%vr!C%U{5n7mE=Ri113)>$qZDCizT8Lu~Iv0ibpJUda;qP>9Aw z&Wx^N_x+*2-kH|NDoYMu#W7r!e2__)>W4m!IU+^uy%f3?mGt1rZybu|WAhf6;uWN{ zUx+t;0q5+4QyGYiU*4u=H9mI$!K8|stxcqV^NCvAf-Q(<@?D9RW`xF-l<6o=JmC(GDY z@MKdb8GuZ^D=vm*{81sWw>uka?oWXbKXe;kx2WW03>Z})Gp8f3UK-q6;4h$AC>G`< z0&fpQZpujwQeKT(I!yx~7kX~NJZkJPAbc`)7;7RA+&td+8Y87eOoRj3Skn@U^>-re z8>*73gQ)Eo3!NMw2*(s81^9L{{f77&2sU9fFrd6i!?BmyMe1ZQAnWWqq(6C<1uWPi zbE^!eG`PS_GND*hYQBi;v%(uveF(}6`AUKJB1)MhGmj-n+zhVLJE^8ODQW&rQvfCZ z1fgO7FUp>v%}r3fQLvvql^W#QXa#N%ko<$G%Bs~{0yS}UE#V)YuOFoXI1Dyi`LO9K zcD172WYY9axxHH(-*OiMI#Sxu?e6)IUmG0O`w|LWiIx&0&mYmRW8gqDV@!-{F!vJ9 zSV8*7a+dqA4*j7D+0s+A8tan0WIyZ^PW+$Kp=g*ePZnn2LWzxrO)Jw#xm>XIU+V5A z<;OQB1B-!?bzfh?%Xg$pu4Dae+sQb)0^f~n}L<%Rm-hc9YsD8(<<#wS00 z-=>c#$5E$#g0F1WvJdw3^wo}>Vhk_V@x=r7|KvQsR?>wR&!)QX& zmc6HkGN6|8=R_r#I&n*BtK8NtkB>lu{HsLs5DG@y2Nh(y9quTR+9#9Y$28k@^M)`n zFv?Bodu+1;+@_cq6d$EC{Qj}s>!SC_Yw%btMPr@bids}9S*v~IPEt~Bf>bNAuky2o za=?Tw#(zd(__&s{-$`j`XyE&PYy69YLQeAqp7eCk5w`@b@*aX?lEk;1_I?Om@V*_$ zn5z@h{FL~2xBmne_#f*}0%tucFW4|=J+cvw$!COkT+? zj)Kk6WjLhy=L^4*X3!OT*X34-i^>jeR&x~umkV}AUeGDo8hXYn{+wvVlic#yApW*5 z#7HCzWkz$-JD#I-=(a)K^wB*}M@QYHWCznE6T^c{RU9-fd+ZpYq~%sL+v{iIBDaJG zSDXfyH0HZ_RJMTmO5?DcGl_BR9f+qd`Yc-faF^X@av%QZ-N>Sr29hdqB``kMi!n2a zno+p@RE+xEwz86(OJj8~>1NqismmmH_AuNyS?3~+kLV03mhoieK8+r>srve)j-g#! zzu(Up{&+g0=1%y-<1O)ct>fi6n|+l-<5pGsDmm0Kox~*fQ%OAsi#gW}7J$dc$oqcv zyuR)vKpxx$kHzt~`N!(1On2LOI@?}TOHfRAE!y@^vxEkdE2|6H^CtJHixiW1bKgR1 zcz)f|q4}DtO0W4&UMqg@ucbCG9MOyKje(FimS__8L#qt7)7k6hJA&IIk+%v#?Nc9H z&?B#}x5_G#dXJww4p}x>?k48(zplgTJybqj`q*DmE)}-Dy#q8YcJ5;yBnq8UAMYjx z#c}bxbRQUs;QX>&UFU0l>Vewtw$a?F=r@N9^V({Zxc+WCPwKYp6oHA+cmXlP703NN zoquKv0~m2Qf!?syz~W^!qrF19M1d7NltBPJWIlCRKo88|+#O{E&M3<&YZ5-6s2+`; z+hVNVa_O$rEL4nnASuhf4c}LT=5vF6X6E_(_)b1d({;LP^3Fz~&ig#BBE*Hyeg z6U#0=)Wh;IR0A)EPeapDnvBnZ1INbrpS7z9GKI8Usm6TWdA&7GyYm4bv-vTr+^q-% z>qWk=1B=eXH{gn}U!2nAS*OyWS@&L)fu~tf<4o~daGjI+90R`m8bjjM@Y0!iyvU@{ zEIlzs^w#C$Q%3+x({k8*`k;GWNz=!io!a#>v=$NxS{}6E^AMu;tD+a0pU-<-L<9H^ zYV%_*CFVI?gtT3!!|Y=%m!f>mcY2PT405>mvGce+SAjN1^QJe7!sIHp2a1Qy>o(S~ zf-I2w%kH8l#{M(L;&rD4|D)hdMrH#`F(gC#<8HLU6@j92{cCaLJHrmcMHFdbvM6P@ z4}5sdy^~PlYu%?GsEOz6buR;3T_RyCNEzPcOV?)|+PlN&M&$jY*a%+c9m(5$Iq@#auPma3 zyUFrW$ z%SdbW+=`4b*;t(`!O!IJISkj?##{#d?m7s%er^Xu$(1^k-z4k!T&={UA6oZ~f=a)M z3$yWJJC8uO$RcXqSNblhNs;;5xqlui+^NT>S#kHg_NIz3cbE^DHZlC-I*#D{Wavcj zjjHoM*sY;aZMB~&j4Kol;aB$=IVD;oXgJ!1KdRAuJ=bW!ipc(kQvtqxp=;xLcKcY5 zOyc*xv$^B$ra2dgd!EUGYW6tje63*e&g9?_Ea5PD!j+}6-G4b|X7bK@AItJGK=C$jH1>y+mCr!(23 z(e8$m3jmfAvZZj2Y_dFG`uX8FZz-v(lzPg3;ahV^ZcxU~6#=-yB|z;=dAdoz9gOC5 zv$v7*@ufK$SJ%?EFu$`mB;V$XJ$8LrkCva#GcTN3^(@{K>#rPB^LZyWRG;_f6xK*~ zkG5&;I>%se`P|-bAG!k!u&QjkE|+YR*>qIDmcJX)-Dl`MME?^4^1@{i^G08ZMMm-u zJ4-y;*Ko*(NZBMRt;e`yEAT9}kOedC=#_BqM6 z0rm25t{e+;bsRni)$3g6Brmm3XXTX7@#zbdFobE?kW^L7w|e-v=dX~S+E|(D_Df_I zy2)DXwHAmZO?kdvd7GJe<)qeTZ8$tw(*DwT+xYISX0U>+-pXICftTzzDqV!eWb0Th zMEIR8^1*2Z<5-lnt!~EQB7OI2U=!_nTnp$5L$VxmP}3%J#MF&x?D3z#VvE{yNky?Xo3YeGPF`?6(F2>GP{dhG`12Y9&D1L8*IFI<9vX z^84pS8gq&bFnDtJ?aduB{)eXxGp&JptS2aaQ3FJn^Wf7&G1A_5wJjI?UVXXr65ONMUjTV9gCq=Xm_LeJvCeSev&y$4VhsTpi4^kh=~r`a ztCMva=>AwKk>Y?$le@k_ohH8Kr-?P<*PsKNi-)wfp(d^UO;0(E`^`>u)&r&RAN51w z`0vxirV|l;)zp!L%e8pv*1-P0bu-qUFC`7wMFQ!+W5{_qjAlpzclGX`#P*9#5B%_)jEfts zc2#s$b~*gn*QR#c_4t)Y_Zi3Sf|g~iI&fS~klUI>ciT*iAmZAY zpm70lIh;o$7YSdq$4Zog^SbPxJ)SSNxz~Y+tH=tut6qKpS40^A`076n^{{(?`ilLZ zb7eGbUL-c3bWDAJTFKZh7Qu@Aavkc-8e&c4-%Q~)yO`W&`Q7?Hg1i>l-dqbiV!fk? zSBl8{u^rz-l^W0WiJY6Zv2^_B!ECWcXCTeXq8=_Xvo3c$+GN_cWQHxg& zWwdP=h2}#Bm$_2xV>1^@$)laRlAmEo$~3;4?C$KaXFp}rT61WH*9&p9;_{SrLRBES&~5ig}s`v2njiFPo3*)FM|x7 zNcjNa*oUw*uq*USU&&z5uAH+$v zDG!blmje3V#&X6YpV}`YUrwjnJ~H~A8;;+FahvAvW2Mh06ps`jmD38{(EM6NUpkOA2*T5>hlHQPfxBFhr^+Z7)n%s2A;`; ztF)>Wb#H(ZuFjRYOZnBe?nyZFQ9ENZCd3+nyeDXy zWqZC*z6`TrMe%(|nl=?mGzHW`HUlvaguTY>ujl)#pGUs8rvPA$(V6+PGINBP3=Wul zN+(aN4+`q%{#HB0>)B)o?3l-kmJGD)bPLa{f{fr`uF`sifz z(V`iYmYmA-!sT+=CiBOjDg+mkO+V`YWaKr}JO~9Qd0a9f4UtRGQRMKGGKl`Nzg5&S zCT{|U=gREc^WAc86)IgRrtJ$;-Oj!LrB%O_a{bL~XPnynsa?#PQ=Bss4(1d0+zJ_% zb^1dGfIS2)E3N!D-la|sTc}1kIAgR8DYBZjP{ySDbDj-tc zUU`#e9jmALkcqH1VR7{%mM5?BMw<*YuaIPC?9wVW-y^7%E3XA<6v)sh#(y1mi!;WS zQfGC>*~|{jGKFL#GWIf@2Uyw}39E5I$}IOkZygM5NUA1oiBWHT5dsY(=pW4SW4q}< z|86lw&}xeMb^>YvvRwU^p`Jn)n14*kk2*rY&3;StJSjQ($tyac6ASFmfBhZ?9k@#J zJdI+1hag40O26%;AEn6;zx_d*!Lmw`xMydIVt1lFhIb`tR8o!VPn5qCipJ`2*fb?% z!y|ly8KmfbZiW8m88BETM6qE;ulXRE#Sc};ow`p=E=L-=98U| zZ(4$6I%?5!e(22Vz&n-*(E;zn;DCzD>fE0nqH{A)=mfB*ki+L?lMt#q2l)@H;mcqN zAQ1))`y}V#G}f)h0&x8r`#7p!HZj)w6B4iRUv{@_m|T?L_wO79kL?D31a0!usJmc{ zh>dQ1Z2()8@N_OcyzccnX@=Cn8TtXMowE)#c@Mpa;POx5p&j;@JlTMPH&v^I;qX1~ z5J%%1!$*OY+b8dRz{|jWn^Gf!H(6(;CN+L!g`{}!sl^7P@jSfIcNfp5=R(s6rMb%W|3KESxAvln?$m5X8%^wZ!t>`U5nlkyg=%`c6>4bZ z>P%)c61SyYcy=5*Kp@3An$i+&6`4;vik_@K2SeZ^XIW|Y)APdwa-XlQljN73cj%_f zkxdzlyp5xw+y6jylegu7_^JzDCdFNHhu1yp|KR{A#T9+2fAAXU>A1J(_MdM>%WHzh zTw;Ek&e~rf0MXK$?*XghfE0H6Um=MmaJau^oUXRuC&a=3t`w2T2#1Sa_std@z*~P) zKR@^gdtkVb)C`SO15^rZEU^!5vr*P=-^)Z_j&BiR4|1>YfpVdL zFV-(!{B^ayaH%(*((Ho86U}+BeXAYi-(03DuS3%fu0T|g*fRWg`IcLnb$obC9~$rf zkjw1j?Dl3Z9ybb1KTYxKk#9&fjkL7QZoQDcpB}*T=T&IhI$_Mr_26t5vE2?5Tt+_5 z&@~A{kt+jl58ZOS!Tc+<(JM5gLFiCbazD5SNF?Gt`*o)ayt$L}VY_98c9s0vg7j)T z-f2OlUTgTz6O@anQn)`xH8sP(IS$f|YW=s>lQ&v`7cI1I@6T6-jkbp>!lr$V?lguQ z?8}(9eR<@{nX0eG%M8fRM+L}zZgN#W>_pQsH3j*SS%Ar%rY>8PoTbkPYIQcFspQ3b+xV}Jvo}Miq^^K7rS>omn(TyYEX|Btwxbw97sGvSzVDO?3nQ{fzq4qaTa?Ng4 zVebj>pH!N5r>Zj=XWXRTF<^bCK`F_>@v+FCKlV5wA}G`Mu|VHtF^oJ!kb#F?3Q0i$(WaRrKuP~p3#6g+&uT%1mmZ=Kp)X7D}2D<;{e}20@AR7H8 zT`vtY)B{V?!<9l-rc zgIE$OsZ3f_{Z7#Ty7)XQU%Hf{f6_1iU1_$YK*^&ws=hj4!Qvl8|3;60f@fdTFVqAR z=Ktl~1Rxkbseykj`u^832A2Z_`kw0ixc7hKO%n#*IS|v()hiIy9HK?K$G54~ zaP0&d!m9I~TP8{Umm7<>Xpw^c5gZg*u3F0^*V@_~+~&JKfZjF)udUS`d8l)1r*DoP zyw9l~xzu~nrx@!JCbhabh`E;Wyedl3cK6=%I1Pzp-MG1|1m&Lkgtd8%uW(O_+^VS5 z`uBG#!XAjLAv&Z?f0@m8*m!e$r9DRgxcB4Gtz>=ex9wwb0zv2;-D`Q%3WJd796IZC z)dnB-_mcjI$-#caSNC=tgw|$-xkt=dJCCV(e_6-Jq0fORT2A84(>M+q892 z6K#s=3d+mMT52l$k9>Hl_Rijb1C>Wq%{kGR9N*P{-wrFqYNvg(%E=nNk=D@Lqlon- zdJP1nI9liT*J`oWPSXRfq6PMUg^HGAyjKPHC6g@_(MwV{|lZas2n0 zbG)&9&%AWJ^SQ|iwhVkczqXb+$u(W8B+r;UA8z1`AK&UR>-jXqSu>X0asTkUisW;` zq($vHdp#W;d)zUUH8x=|m@9T4#71mrpih3;E#i^yLJL{im-uY?SkTtdbkrEJ?x zysRVBTy68YY1I%8uzK3(lIE5|V>*=3Z`#Lyn3wt7r`Gaf*qL z)ARnn8cZA6HcXw@v#((I4-3)F#hi{N*|IZcfWZg1*U^Sgw8n?b7#V`~E|OfWr5zQ7pMTV{jyytbYtsua z|Ax#@Q8u4y*9#AIQTmcGys^zZ3nn|-H){CdYjw&EyFZM_A*$?C*?4Z|tN-31q%A1W z43Ed$q>>-?FB7dMCNTF_VHCwq{)DW|vc_S_g#@Tjv6+`aLDw14u6?joJdQK?^JIqT za4?O}C!>;RI>_I}>^2`4&lVsO)oS6}VD<=zj}Rnq4NkIfy-^SMWh1$+*B=6lJ%gkG zD)_rAfg4U^Ee=S5=-DQf-ylgo_diO!D+*KQo2xjN2b37=%(tZ&bRjCLa0;9AH-%a= z;^CN!RmBPIr%@A&0M@+fjUWf*%P|_7YY`!ip?gZWk~*pLTg~c+IcGW}i0S zz?UiBecLKK*pM<-7N(e@>+WE&KR;5wNL<#+u-BQsvYKkX!9~lBT45`^d@kS{+)E3v zIvLscYraI4(s<(yk2DE*;khjd!cKOrOI~|(6XT;%_s^TMg?&X(#C);zCNlW=L z@@sj*3OZU2@?AoF?Ei<#1kzI~g+LhjEf)1w4)DDuDC%r#)D={!co_1fh_r@t(fxl^ zT3)5_+A|8)r9M}u%g&iR-~v{sGv$x3e{ZdjudZ-_1tN@LEoHiIk?-;Zq!_nzrD6th zlCxwi*J}gvYiOR_0V-KfibCFZ_7u_wb9~+&FW+VSvKtTQD^hU$rLH`muAJil8q6%) zZ#E*MIA8m@g+XOia?tnIbKHElwr;>~KOr$CmQkIsvGfkg>4=QOWz2HsM!@9Po(ie> zw@+Dr#B4(?A5RUQ7GDnaUn8UVYh_Z;2RX@y`&iDFSQBnBRZ|>r$lx*&Wcx76XyB*0=Z#E6J)4Y-%HEJKNCfd_{K^`%&n7-U>b|cW= z7l0#WLpzNoY=+g;XIu+}ft0d=4b1CLrZ@9-4q;YY&X&O%it$nzt8?2E1RT%TZ2*Bj z8x9V@8zI0V#nnF6)TK)7Yc#kL>`fJB;#uE(oKB-PYFgkqTKX468ea~~V(3MQ7L|H3QzoniACEPXY?Hex~}A z;B*D08Cr#YUbfS?&|LwIerw6V5&K_IT1|Ya1!`y7kJaIezt(Xw9FXj<@CwkP{Ov{; zcf%&%LMyE>Kq!b1+ozQos>SS9me(C-#D-sD28Fj2yxF;TPD$H-@Y`VyuHRG>Bbr=+abf3%l-!Knv$ zUhmJ>sS|o^PH@f87q3TA=-#tq;Nq|T;6X0Swh3(N?Jly14hW=AqKwm%TUqdH?tpT_ z?*L38ZuleL`UDMUo#2u!&#vEUh7pmoSB%>;B_NP$2k3p#%&qU19iqb3xS@q)9)fCM5U!E40MEa1s& zoIJh(RUluK0Z#s(dZ{2mU|s(nI{h5wJMcMP*^S;BS8MweaH)m^r2 z+qThV+v>7y+qP}nwyiVQ+IydSo*Vxr#~fqk$jFTEjR&Cb&w>XULT(y$^K;=sx`5WhK-*xl~p zs3F5cSeoNF{zBs^Xs*F&hbT?;hNw4E)7t@AZ*}YoNW7E1?ZkK6*yX;#=hx39gL*gMHgMg=AZK0f} zhHYj_@9%rvHA9vEy`r5j){#>aTN$qrgpH&=|IVuliFft}ZPJ8oe7s<`k?c(rJ zeyUX*{#r}pG5t#0i`f%G@(bXV(yn2q<+VAlR!qhXwkyr_7xKIc7%%q8Y<=IURV-8BJrhy{%jM;$E(+crXjdfp|ro$YTr-k4n7mmsh9qPI`(zM3)_r4HqBXcvroaT#UaWj?Wj_K3m0RA`{qJIv~ZP-@<#<^+svy zRNG-YJ5w~^lYC;z1k_!bJA{Gvo8!J3bXX>nqv;vUya-bZMJ@($Cg{_rV!g%sG0(f5(oPRCuqZ{~W8kV?yx35P9{JCKS{zzy(TXY|v> z^DJ0n1lA?H7o+Wj%TI+aR}8M^LWtQffA90n%a6Sq(j-bb;JiFju&W1z8i#lF=dqz8 zBZUiUzk*$JiFwGi1TR3ZCD%Ev4x1IXh@&GcR5V}8g-}t|wf{|O5LX7ayr1ZwWml(k zNHF=8J2{Ei1b5In2y#}>WnjAiWPqyUL>M6?U{d1WkOUgPc&NkDMCx;7NA;XA zHj%dtIlF1&hL(bzd@Q1R9`4bM@PcmL0gdx36*}jzb_JiiRV<3J$t-WE=UQR!ZO*bM*UjY0_x^PtJWC)e?YWgb-kRG zC)c;ImW8SN>Mw&#yCvH8gXAxNFIM$j+JiVK*|zsN#adoW&!v5QUuZ*Wp^9Q;ck0CC zljS(p`Z}Va)`XKe^VNz(5+|V=J}@S!88U@Z4^^!g^5+lvhQY}H&B;?hMdb%Wi$Mt4 zz*Bur-|Xa!G7$_l{^AvUDgx9raCSc8wQ)B68-d(Izf-Tf;;l0ywqlv}g#Kf$Y~iev zo)-ls=XaQ+k%`bAOI=g+xGs8F9?#|CRihYQp{CJkI+x7CgTmr6?@ZO4&K z>*|K(D8{>j9lIhq53@p>{8DGH5AtO(QoU5uMMc%+tK3gQYfMd>zcApl(46O)mE-*h z{;2w+7^&=!ntZ?_Sc?GFFdcYpX(;|^;??km@3Iz8{g!*@gb+N&f~`oHWal5jg7c;G zMNI)k&{<9}fCUx-MX^4@9#^xObid7fp@T1_AHlDzth1k0>XQ+tsL}WsD(muddsh=y zu?kMpX1nO%EY!`HAmO>H^=}Qkj}!A$o3Bu=iV1dlcmC)UytKsEgH{l_mPrlu6(}|v7M~toGsAA}Po)2R(5wy^Zgr=>O`pjD z^0N#gc4a<-d5ObG)v9{MUFWeqQ5&08Bs; z$YvwCzWT*v4#zcMYxe!6%^)@!CO6dYf7e{>b_+Le2HYVTSGt__?4{35GODl>bTqQG zf{eydcO_H7aUJP@r^4pP{I27GL*g zX?9-CBPHXyR62KpggUO9p6Y51{K`aZjSMT z)ZV56B=Ef&W*i}};Cq`u5#G|hRv8WYE%e~O`Xi4sl(M5Sc1;+9OFVWdEaTf{O}LWN zqcvtDO7!LeFTL}sbHoRKEjQZ64^DxL92eAnV8b@Bh5!3^pqe%C!Wh=_nHy+z*j3dM9jl;(XE#~hHODyJo?!08#}GIZyKeGo>zc}Wjs!Zd&FlSG~MPK9p2 zji1^xAjKzqRJdJ>HxkFIU#YYp&KSL>PJp~-=6x)Mg3b!yeCL9`aisP92(9}#3I$cA zu!Mjk6r}q}lYEdM2iY$9n>E(HBrhA;K9G)y@naeI55 zAdMHz?LXrs4J2}F9y58AUhk~iOhZ@9PixItuPU3s>FQ6RY)%#l@NG!wd6MvF4!WZ< z`0R1E-sLb2_O>_y(6ckw?w%Lx+&CyV7+=-z)lzB5BM35fl>_b!RZOoh`X%p)Ji9e2 z4;!u@5bkI3eUIG9G|=cJ7NoyqwCA6-I*3;IPy{0Ch$!VyMMQh@L}{^?Q)A|nG{5@4 zaLoIFFhVhhG+G>;t;a!S@I3NV?!KC~5G^IGh5ql)cJhglEP*rp-RJ>ZysmZ<@IjG@ z_>1n0*EzE8uj-!dovE@p_{aOs$r-T^QuV4h(LmVAr&O+}r~pxZs|Z65k>tUnNpH_5 zT&^i8M+~qIB!*vAuzke-j)7eH5)oyLynR%qj(t(vi3ehkc1Gugn~Q-nO(bK}`Rs7D z4+iH!haler0x-aK1qJfCejcRW+Ph6&Ytjcl1wcKvyVy%f*zyJk-|9CGwLWowa-M(+ zz`RliIeG9H*~u{$O6BI0BHYuTP5J{{c<}U;Ls_9V@-|iC<>@C;MB<8i{ahZY4YXIY zDysT8)%Tnj^!fG09_z+FcKj|3JSi{8|G#5{{&J1{6~Y#EI@6x<0h5uN^j8^$0_#IF zO%q{6meN*z2(&i%+YXv%xaRK0?r@PLGbZFzY>L`7iV@0S$c$~AvT8}bRILEFs4=7& zwi+UosKe(~PU-#7?Ke9ppm#8Hja%|No;4Gc^i#LaZi z-hGArx%9sz7!(VWwB+Mllj1fK5g9e!-+HJZM*-!4>q^?L^S!8z{iX zS``GNE^Z5Bc=N7IkI_*NOi+KowosYdjGh3iYR~F4MR?-!M zF@4DLzz^QYP5&-zGwIPF|5vU*Li_KsXYYFNyrfMPLA)&{2-H~S29E-iyeIGMcX*uE z!I#}wA_#uyUBAgjeT`48bu}5tonD?D3f9Z(>6q2^DvE@1Oj0mQkOa_1=g~j$JwC@=)|4MIU0F#}n%B)EOPJ_*&}nIOLrUmtoAsqD(Xm6UBrs*86tLS%v*}>%tVk*HfnX!|StQ0tIrr zx_BZ*0$c!KUOa7@!42umONV~BTx^UO5!}$Ym&1t1e_VD_k)~z z`sEM;JF@8ZRPh!i9R%0O<=Y5yFrk7tl~=On5=l1>II}JhYcmAKRvEw}G5#%$&ri+38&yWX++;?`BUMBb`{Z!cpC$QRa z!VQKe9y)iRR`63wVtru}QazHa--mx^*`8G;Uqg0^Bu5KHg+s`e_HpV*RzJbz3HdrJ z+oN?n=(6@oA99Q&FHCAZaE#hIPdSI@(`_)Dl|-pkqgtRP>_8hasw*p_h!qne^;_^R z=1^5qDp8`?WB<05io8J*q){azCT?wOWf2kMUyJmqCN3)ErTC#z`wHQXJ5(MSn`bc` zO;lE~mTr$DL&ecyEAKC^%Bt8Tlk>86+aKEU2+P=2vM~c5E)ZV%#>VV}MjTD6ov8fpPCm-`eez25Q?*f?+F z?l7qScMp|I?b8~S4cP20c+DH&JJUP5e#}i>Z#W*n|GGZHNt&x^El?m&mrlX(7s=Ph zK7kn)Cqsov*(nqzyv7rV|HaD6N>5+g(XlZ)IyyTWJAHgUl$4m*OXjK%5QWUlUUW+F zaByHx@fKD$H#?Kos=>>m+}+(l{B|~_GjhsGOH0Yg$>o$~Wo0!rH5ZVrwkUv zt|y5YQIrcRT~V?`cqA4hC3Ev&War5_WFI~?U3dR+`ptH`b^q_`b zJw_Zn)Za4fy6P(jN2WV} zpGdi#LXC!o78De8s9)4yc{fhg-fXdag?Fl@rq)7wpz&SmU3nj++eO7dPj3!KEX?O> z;hBfzme@KQv#$P>%Hfg2y?goNh=;;r$Yb}H?&BGCc|W3L5|G~x_^3XdQIuMxt4hcq z?}slIrf=}_k)tZh`B;H{=bQ%7W5{R;Ww(B&1!`q`!Uu@O$x;*p?Bis(#IHUxyb!0l zqUBaAIGQPA`tw+Fq<$(v{UnI8)fkD#KtnhYJjt#zcQQBf;grX;6ST@_m|Y{(fY&_q zE!*F-Y32B^zuiM_fcc4#`Qu{K(3~temHnZmq@^1Ta;M}-62p-b0hX5LzSHA3%>sd~ff2?) zAOMZpIbZpc7zVvSj_l7UD-%Y;V%$B5CYCzggvlp1h+sxWh8`7F5ON`P0t0r# zO*s6!j8`Poh~KlsgNy?3caxL6Y}g8j1Eilb6LmBfGIDH;td@NRgly{Cc#>+lU>j!L zQ|V_zDrn_d5dnkoA6W+ODBwg6FSnKJ^E_B96KaC^I#cba&js}W%bb`Rc$~1y&Kk~S zg)nH+Jmce>()><0B8y6~_6Do(AOrA(z8{!3#sazg(W%?;^orFs5{%xuwc+dU=d>Rc zr66H8_-}`5vw?}*?8_N$qYl;=I^V+qrpmUy)|Y5Z>?4AmN)c$HSKDZI&8Rx|)ZC=g zP))?Y2J|hK!r#Getp8nBw@GQerlKTB7f8t$XE1QD1kda7df82{;l7(Qen{^}U$dd< zh6cP`9}nWVq_x>bctx&5l*@-V>&?B)To&(T_fx?h4dOVZ)!8<1=N|ZNY7pzsE?u}M zEC(CbWf-nMZ+S{=s7pp?Nx6M)tEd|3useQiS0Lwdyf5hCcyr$eo2Z>#WxH^b1)tex zzR9L7Oh?syL>YTXkx;>(@i9Zoue>EjuX)pZl;8!2_K)I(k?KlN&u+=lI?eZv{eBJ- zDu{^$gZk+H=!$06hZNq5+{F2EjZp=+akRM3A?MG!a4({6#4(b^(fA0(D;9THG}H%p zDjq7)&0v&dsc>4N2Dq1wNWP48O#F0Ua(}v4yQZ8A4fECfOwQ}=?rj&nm{4D3&+lkS zaYrdE%N_-CG)qw$%Fywe)Bdv7)R{q|V~FvfeCL~A`iX}vC1XBU&TgW(+<9pMlWWx$V&yFV3A1~Y8)bG)R$-{qNBj?%MBU>jvA>1(-@7KWybcKhH zJ77!mlE<>1FlvG(;z$BzbC7G>KAgyl;a+dF&a;}~Diz&~_CAnSlFDYm5npe%t9E;_ zRyRS^%{h|wE-$mPhD;e0%VWcsO-EI2Mec28|mMp^o^443;>mCyGVbaOb7@@=3# zUOIPc6^rE#8)s9;$;FwiwJL21W(QJ<{_l^FEzTlgNKo6)yQxeQ#vx}WjFSPeX`2^% z3Pgu9#XHgyS7SH1ATIp#6ESw6c`=(y_=b zO&HK?ZmTvRe9Zcd`I&P1y{M+i;T=ArCJv1hKBo8``lI0~m99eC+EZhi@2?a_3q0OpKM0UPC6a)DREB*&N^Vb3ItdJE*czu^)capWJ_`Ldzhqv>FZ|e=6s3k+1&U`(ZTVQ;ByD3I z!Q9GGVcra@b2uM`SangtfX(v@>W5%)DU`-R9Mb~zZ^KSa0Yp*yLeAO(Hb!hN00p44 z$uZrHIB|G7wnuMa5u$az=E>Fr;ejQqRGf95htrn0{xosf4^-l*rpsCm$52dsCTG>O z#m(0?yY&U{5QGK8Pg`NNl=vjCp9dGa6KE~?gtZY|^raIldvB46g5H_Vtn?)O=B3(f zn)K>Z{v-2dFY*X z%Pp^7sn}X7z^CqljjIdZm4&`ptL%DL;7lgT-Q=zzh9~=TzSxK*_fu*B!-N zmHSB$!`I_C+O@W61l6XSR_H{M2h(uct}Vutjkwm$Sm851QQhO@vskKbJuk0K!>iyp z53u#s#Msejwa6_ahWXw=Y*f*2aYk(F9}VDl&Fx5A(Dy#8lte? zdr$iKD|q6GrqVmRoq12ozIybkQNlECkn%3?x6|CZ#oY5p2VRsuu88qQ)^>iD9=8eA zrNuaC3+6H&`EJMPg3oM4mm`oCE|nxkRa?3)44>p|{(^lECcF3FMj*&9l+Jzh-qhS6 zV4UH`3ja+fz2ker=6(FJ;0AraxdAmo6V{gGoTmZIeyLAYPw^3#;5&?pwsd76_eLdD zAyKa0wP0+ zkEMvU?4^DMTkl_0PP8Ow>8HcH$g2Kr@=R}Vm&Uy4o7Yb1a|kv%xdl<_vTJ3G#mtk|~M<3C`j=bd~Gba6l|`z4Tv-whSsw;3I@= zxtzEbZ8{2NoU(z#ZrV4&$`?NVImHS&f9M;F^SVDmJP1RaLD3t+Kuioi1}cOfvBn&( z688hk?NR{FVT?>Pe=I?fG6al#!6+y#Y_N{EcS&k>4%=(=x*hbAp|DsU2f?lyhBz9I3dVC23FUaKZ}9xu`x)hKs#Mg4c^Jx2&Q7o1B{K4!0yJVETMyS1 zGwd!aMAIgQ%dQ^_UfbaZz(#LZ9H;G^NT=hwievpYcpJ79m1AY!E`sZKWE!AHu zfrlrQ?)OmRdwa-_hFKW`ycRwawDv;i4*f6D)gCSJWrarzT*z7SMwXv={@I!g1? z5p;uRtMaT#&kD2$D0~Ra(&VyPrR)z29h+eYyn-;3*iX_UNWLwiXq>w(!VVNZ5G8f> z&{8(O*bc8#`4!HqRrK@D((1zWVd*n=g+>Oe+??Z#ckkzJUj%O$`p@i;J09v~EohJQ zFe0}7*}oK&l#C17E4`>L(^OmNQ_KXN-i#MSV}VrFZrgf^#%T6>Mu!{laiTw2Z#CXD z!zUgd_Sb2GMTaKSm?dSr&%rpy(@meyaQ?!sau@p;n)C$)@h_EJHRETb`}|6h4Uf9m zv@i%cLeGS7vO@6K9VgURXR7g%v3Rf1KRT0r%4v9caeXH52O8mM*Rw(HiBk>4{F<)3 z*q+-EeGdcQ6OgIKs-H{~eB~qjtuuTGbXe&%-ZapcJ7E50hDqTrpa@t0r>cOPn>D+k zCy7?G5gNyMy|o?>g0sEh^WH_ERvf3>(i(-OAhGh&D`$x=Dtb`sY|{gILGE(a7QfXz zQSz)a-l9KBSJLD%jch)`h%K_nfV;Efgbxmg{u|-K)OZpo%)05wmt@MT$o|W{DPKqp z+Shm53ZV?Dzvv9RNo(XbjWb;!rUdl0+C^de{ z$3dug%dzuJ9v=Vf(rMc5SHiHURsvE}Q_+k}xfEYGVBzROk8+junegd(@fl4f7Ryza zbUng$H@z%)q-w6osHv^A_j?t&Xbo-IaN~~#GNMgR8CMi8=@KNyg?FBUsdss0Pc3W? z7qGtWmEoKZE!!O%0SomQyP|>uwx~4TmGumb7a|XT39Zd+<-|k47W)FfFquac1PxHK zY+gP!^KGHG_weiUUD=*J^+&)4LYRMnB6+{xg&Tg)$LFX1P8EC%D)6`WeXwh~VkXOK zvxN+6^ zDnEJfV2n_*>erhUDKa?ERe`Q~sva3Y;i`L?OFF1%dc{*%GCQ?h8$V)Gl6VxXDe+_D^bdD0$I8_oAz0Ox zZl#w1y$Z{3oWj~?$cZbt9TNi&RhWO!?Du7<#U+tnNd=AL7lnn&=P!kn2e~NjZu{x=%vpt3p+~Q3)ynS#NexF0hPPKr5#(&-P@QM?PNpp_?LY(I*(NwNFe>LoQi2!D&5Q zq&7Je%-f}2H;P>G)$>I4!8C!I*_q<#w*^mJW!6-lD`O^()0$=^DxthTng%akAu9u04h6ycqPtT97P7hk$E;#DY zHD-Z%!8AAL`K5ik=4+mbDVbLECVd}+)iIQekEzcW)Alo?~S>~L_BhHnVpksFS-%G~ag@Fhw$&72}_ zAOF~M^K@a{Ii1mtr;)WK#nqTaHD*Wqk>HjklpvTko*oW>i9@;w@lyRj-{7qK(qo4k zu&9^iHtdr-Wt|T!Y@Hom7EaV-YkztpnXRX{ySOc_^dU`xl0@!n7UAmE0OYF#bVMKT z41l)Mn_3%TB%p3^w(aUc?_Mm{2)GIYt1P>XHl44=h8N>+q9()6nud)IaBt%;xN2qat<7DFv37kN%Y*eq1&M} z2$tx6+ZokbUh?an_VK{`3hy0r$0+Msu1mUeC^849=^4raX-8QE% zgQUoAR8UKgfq)?U?|C$61vTU+OS|Vbb^L7p+9{(0!SPI}u(5I?R0#m7eKo?0K~+fn z(SXBf;W^L`QV}p%QyHF6Yz;Jxw3*f`V58N(-J$E-c=>m7dtVeq&1>j^F_xS`v7TSD zjFFJrR0wilz_%kY-XbIk!J-?Y_!~{-hgXyUiAkJN%wEobwQ^gQl7UfxN%sDR^G;M@ z7)r7PKs!CM8YCO#byLs1TFcS@jWApQ*=W6OF|{|=U9HRpk1vhQ0{cEZ`S=b@9Y%XK zK}Uc4@E|malUw?Z0^yp3=0icL!|jg6>BDl#=wQEua`UMgN6l#vWld5f^EqpgQ` zSI|GG_g1a{cJezt-)U~Y1!`)DY)kKqnVIghSn>NJ%26GT@va5#@@0CTenE!|&Wsfn z+4viWGH_Z=$I}xj8@@E_z(mV&$6vQ25*mWvI5&%L+8f`8x5N5icCzA2!axf_yPQ6} zRGnks#qYj5{(8fX(er!K_Lr~uE~kJZDUYlR*)g~muV|Oy@MO&=YyU_>5*=|2&$#cw z=;UGWr-oX zenM`Oq#rgD*WLK=>$NNqE4KrK685SiCDM{b`C;z zUl`>W9Ashf)PKCk)MCRb9csaT9xXH8*k2%e1iLU5z^OhmIj>(h4JtbG!&g@lXdlsq zFWhhzn+}Uigl4yT7ur}nDwI-vn~YQ}_O?57va=T!6i8*;o4K9fCU2mWM-`4A-XWkQ zMK_3Tv6&1iyLG>67CSa7J=5pC7xQR-h#q5cLiJY>^gmk7)EHsMkG1Gt`&t9?FVhXHizUnIZN1m&GvhJ)-9k$YHn92~ivv3(*=7UBwTD@m(U z7x>R{6~=@T4gLYfnD2DWpEuo6%Q@cX_r|G3J~%eR^GsT(+)ncZo%54rE*7LbsxX}7 zJ*|$XeN4cq-Uf$Tp8lMcTc%4b$OE&%A%qL(|5=`C8d^)RWWfvO?W(bsB=VSQvzf5K zw+F|r^5_g(oYXugm69tz306`FF_e%eMxp|MYH~1k71Vk+=aPzoSPIxV=eO+4E(k*j zdv_8U`J&9}$xTd~D6Zu|diS9skN%(K=ghG3+Z0ZN*nh;aOqdYLu~4(<$0kSUamTsC1xLp~=e1iqRx{e0ZQlzBWRdTx++{)miKxSsA`*vskWLaBintjUf~q z8yow2*^dSk-AM_CC5p~7h~#i8dg_#HS3Nvj2c$;}55nWHP_ePCEG?;!!~+_1SZ#H@ z=GGNfjaR&mqS0q*gsfbntL~e*yu!Rp=Q@E<2|T zM1?m~MFfaEjCb0NWG;+sgLqO-#Wbq%YjR4}Oa0;u9&N1y0_zICF?28|-rR()pC*fiaf|&0H6br@K z4EUKZq*zJ`BU(cUFQyPcAfJck}Qwj7L5QJ5Zb2E$D59Pck z6yPvd`D1#y~KUr(#3I7F15Zck)7 z;*Zispu#`ewJ<>-MWuy=U{avvvf=@Wi`>VN92@9GxjM@M%W@!2@P`%-i*egA`n%JH z<{PClyeD3$98}!>h-ta6wm40L5{m z@4L3d7H`d-BRq7;C=Pl&l|L29_hS3%uArcxqLNfuS(%=mp0xk#X9oxNX{U#wiDKM4 zT{U0@u-L1`=Yw$|a``t7x}G7MOBD#BS)Ec62e=&~x$m}YBP3ela6+ZYDuTWG5=Foa zE=>>p7JrWO!5qAg6F7VNdZ{v^}BIeo%;lEId0Lt#CI*~^&Il^}$ z_O)Y;=*xnyOyxfyC4eF&gsBcqHYo*_Kk;wn#qIpgrjw*@K;xeT z0dO&as#ik7d3AMl$@pTjcn2`?z2QYli0Go`D+7mJwe$ewKv6*f@Nxr*moV(_pC8RV z>1S+3s*53hhA_g0M0%JivCgMTmRlBw$V(IcwRC9$uew-%a|CWMygWIB>VYn>Vw?ts zhJ+Y)_V#5ig7AMlSX#AYYSB}AhKGljmYSeZ^P7SKFaTi2&D!kjECAX#?Z(ex0zMdW zrQNaxLvDF_dBNj!=JI-5n47a7iop73%1i88>8l=e3=+^cHZBA_!S|EzTKZRqm-lkY z2K;-Hj%`Ly_$rp`=clU$I7w#sUq(x`llim7=Xz-lm#ncoYN+b^X>j7pMZ|d5+O;-O zi3J!>v`^jHj=#Tu0R|N`;qTx3Y9uaM)gDW?jlxG37O~NQ^1wAUHCz)7u+CnGRdvQm|W6S3MAJ_0z;)m$? zGsvr3p|iS}AzR*p|GeC7Kw9kY?;lQFr1*xJAdE-WTx%Y&nezeYQ^Rbt-4#GFJVXg) zWg2kfJPSw=N^v_Jx%FX`47^GE2VK>ufv3kq39-@A-J4POro8j}0m{}t>N7{A>+weh zwM$>HHX!62hR#`2Wi4FJC0{**xJL(J-mqah5D++Wfr zkwv6i#Qzf{ZrJqs;(`xx_e8A*Wt-ncc?7@XdpGi%5DEHK2qbDThFsBO?JUciGIFbl zfhtaGjlTA4LsDPpdnzxgI6?A(&qW@1JuwW=x=z#dD8ys z2G)|pb<1fRAfFEyjVBz`$6-;@#I*I0d07+wkR8cxCNg3MHJ!L=;;ZNXM`0&6GB(C) zvSnpFIt+t8&X6Jrz!jiL#=*o)Nlz~>D!OXjviu&CnP?$P3?nr&UkRIn`(#E%h7CKE zBc(U7Rn?QZ*^0hQq_si1IP!JD_pMB2J0;b$;w-5Y$+TWuI{V(i2On8t2dU&ti zk1XDal?J!)ij0D>1tl_El_U<6fU>_&7x0(#XhlUuN=P+PGe7C*Q9)3&x;$UA^mb%3 z(>HaxKdkA?wcDK&G;6Q40W*cHTvlw{a050&ks+h}-EH^2%ZL8^y(1!!ZEXn`F@?m= zip`$beZ+XK9TFko#;c;&-}^jaA*BMI2yFc9}A2=*NAI z=e++>bx*8#83C&9)F>RMJ#lB_^32ucqeOk24fpPy6C%|hmWsA}(^yB0=>q-CGK8sS zJ^kq`Jg>;1zKoZM)Bh`vca}|;=&5-;{&_qRsLJ)wVis2?=YIww7%dgZicfPwM-7w*z96l<8 zja8L+lV&1;wzd@drRPFFiKvN_IlE?oDW817*^KY-hzUojfF zw17}y;6}=bT5yc$q3!e~r-r9f=t1X7XJ+IILk*Xmx!lBA64>qb26}q<#)`p1mjSEy zJR0LCtX-=<1^J+%t$*{_c-rhnwg$XI`E>G zY&5z6H~tA^72MxmZ^pk_j0P={G*y}U^b$Wk>%!eKgr+c$qoH4gCM>)|;}k{A4(SiSg8UCW0OriteanY= zq9DvxOr^!a*?b6G9K~%>F4%m!(*INqYutp#&3zf#Z{!e6OmX;3X!u-g9Z>FHPa9E_G4NL7eUiwAK}_lVooUM2CCV5n zao1a$PfVrVkh$GuH*>GUYVGvlLrzQN@|Y*3(T4WeCOnS4L~{`xruQu07Obvc$41Q3 zW%!lc@!q;%TtB)60mLGTUj*1r+56*N-_ zwNIx~Ap75}a(7yU*EU6m#)#XXTFoymueG5eh_4pA{h`&eD8k}mbA#dD6|8es@=uIQ zuGv+o`HW$>NNwiFw;Xr}ZoZGYT8p2`VX;qacV40wOHmCKR1g7n-`3^dmx~FUs+dvA zz$V4lGA?;_(Rhx=hZWoLz?hwXbZsFYmE1h_?!HDTzv$_F1I1~Q1-o4RPFwE>s4@kr ztJ1orMn|FTTL5bw!4QN-+?Pdr*~X&ZBJdGgOjh$vV%n+u{&COJI@UMufzDE;eH%XJ zC5x$tqNp(72bwYKr>&8^H7jXnbRiMA3HAn?x+3dEZYZ$aG>p^uwx5l28@P21fZf~_ z0AM)VzIb{&A?2q8nUo}g1HGHck3|UdGD)-A zsQw-p7PP-$us*bvEY_BU64){rKUdC( zO`+V7p?g!WOcOjfTpEuzLswgwrdFy{33#_@oSlND4ED-Cc?31&LC{WjbDiN0TC7}1 zmu5YlgL*dkNW*daZG$@2$k?VB=bee9IACi={wOD^^mHReRq5QRHmW@1!*D}i6_#q? zl;|@o+@!^7KX-Y5wy*SJ-{QJALN<>+!0dNPR{cB{NM^r08$-u zdZa~LGKKsn3xmk#;^?dpdSfCO_6rTObTuP4l)eXM1*p^Y*&Eo4J$!~p(fO{I;Co$2 zRdBTb9wz{#>-{`SZkye8ykeL$x=;mJ&dp+b#cA2lI~$|pWFo~IFFl~{`a*q&U?P|KxZ6lEV1-c=fOZ z2YV`AIL$OPTuS}B3~|2lbFQJ25d>dqB82rkOVsQBIq0c<&q$;e(uZK7Amj)SpmY~9 z3}+OUQ6;T7XS4T|OL~4iaj?wqqNY^_YZoo>o-zfQiq$vKrqEDmWps8qaw9exRdKsu zzl*EXLlX+Wh+)E@6G+;oh4#Ub?$0^FA8`p*$)rON4(H z1&OiFoRmfiYcwQc7Cb0DSgtAHnw3rFx?E^{j+7z zmTAD}O_qHN&sp>tYS*kKnJX8_yYS)5Lyn)gAyh1w*=m{v;W1LHg1`CTRkLXbUsb!a zNL~U?s0wdHEO}~folFEx)}7bJl<5kYT``@s;_JGu8PC@ID#(iRY4uPVRpJE(H65mY z{fl*p-QHw5R{}}Ly~DZuBNVrEAOitMTO`EGLwi65oro99WERP0djmoi3`lTv7hLA! z6xR_8HQJ`K!O0pL@T4x(ySJ`|wlYyTj%h82!&O+%{VtiGj9gfHV~p=%VEeNtw(}ZpUH1cQ>LzfoZonuHp+Ol%sMJHX81XEeP^ni2D~D zZyk44w$gc+vPy~-gu$q5cfSS`(omtgn3)(^B>PkV3_w3ik=&C|Dd{SbsX{|1f%G@D z$Lk3_LmrUmNcvdZl5u~&EQRmN+o3E)207R(99g!N0F5oMHr*C|QYr%{mVu}og!6d4 zc=8lz0mS~G4eI;qRl?dDo6UxLuVLm-O^%27%u1pUghbhWEr{uYq|i`g@U&W(V}Pa~ zV+{E>jO(StN3+VgI=LYnb{dFhh4b5fxEoT9^3lfqECnALcc&mtK6a4- z(=*UutRf10xW94CrWh~XU!KC%M>+|}WO6<{t)U95u32||3i!UuW3gt-eo;eR$*5kh zjWuW5?yQTP0BZ4iw+>=F)a$?7M=5NHtmLQu-D95$R|!=?a=Ym5Gq!_F!&D?~YJ;%_ z3!lG4$NFb~=pK4CZ+FO3Z@w9k^=&29kQ5MqaUoExysxvC5WtH3!c@*{6s<(I@QQ@> z5wnoq`6o5hMVEvAdtXznfsU2c#lb=Idp~W7*Jpls zYpoad3$)+8Y5ijoqBTQp+!s2kQp-?s$oVGt`|rwj_twfp?(z>CScKK-7{yVEJVuFC z*YX@2U7Te8fDSFTpS)Kh%T?OBVCdn1l9RssFe`6pn6t|LSMZ?r&^?cj;O`u*D(>wY5+ z&b2w!hY*{h{vWd5GN_IQS{5a^Tkv3k;O_43?(P;KxVyW%6P)1g?k>UI-CghGJMYx1 zTX%}0_%VCKW@eW3>h6UsRXDz)VtD21s$zMzu1ze2v${4uF+|Ayu(U$IGLOGdmb&Y4 z0k*Zi*sxGNcDNm%SnlG05OtKPS_I#|@}-C*vf$hF%AywXR~KMDsjpbR<^gkjw*A}v z$Y%Y=M5pT&W!7{o9R7h??4%H?r;A?OSA#GMto7w$FoG>g3+KI_^7;#7oiZgu$C9oe z_a|AnCGe|jt$ORIAw8%PS$j_r7r)_YCjTK75(4jK7&XCuGZvJpA1ys~g2OP4o9!$- z)sccY)d3y;iVCT*SM?<&(2T_lxBi+*flyG{C!D`LZM*XD7j${rC`vl6OVn=-i59G; zpLE}c)i1xn%Q@&iPxQ6*$!WLES>9yR1W?RE&~A?3;p+1v)d5PtsL!t|(T z)L3o-nWc^tZ3h$btP$`!k!t?%&2A`vwuK_vPHJ0R1SubF^}%DGC}YuB=!6f6tAw`S zb;DH^Ui=0kf~`S*5xD8s^(PXGKMduYMyan~i<1aM4Qs`e$rh~is;v=GWILQ$0GOO0 z*W(@Dj>Y-kh<3OV31Vi*D2<%Im(!~g>@*afE|Tn-B+G6d=>sI?L-Z~1yPEa1VI1wG z!A}-v7pR^ikXd&0_T0H2KIsN8Qsv*~M$6wDUXcnsA_VL*qdIGVgnb5z-LeQYs|XPTIMc?UL9lfHYcHsJij3SCCdQ>7if%S#6&henfLRc={SpE5s{qV6BzUzwZMWIZx`%*@ths! zDT9wy6!yJ2Nb{AM_V{&!EL4d;TC#c8Rt&qNgLe3g$})$dPB-F{a@+8eKxJr2aGfB{2e}HK_;_ zP%x`0Yjl%7r6059S z3WCcSGVz_46l4KxvEmycDvB3G##EI{JF`8~%d$yziDZxI?NL7pTH|i#s|j-=@t2=q zyv3c=gBJo_&4`tWHj_17);YBk-fTUt#kPjzH4zR<(q~wlCYs&^>2i z>qm9v3dx8SpXoa1d_7=i@YJtwW#w@n2P&Iqpl5HqL3>M+(AJ9ZJ{uK^y3WU9Ekz_V zLv$ZenMJ++ZZ0Rgftr?)VY}`z>7;wPz%w*S{P0~KjWnxm-0sBM5Y!Dm@nJ5zuJZHL zs632DlCz2C)lz@(aB>nqA;qmbcc=JSCE3)P^WiIZN-0)e6Fy{P!w|m6B?2stoL2U_ z+~RV4BP|Rg&|7cXwZ%nd>4&+mcT&*8A|IaA{F2!J1Vd#XqYiCqnEvdD-X_#Vfd|bu zlt9Ht#}P7DEoEnf=f3(9nE_i}W)DLTc|++tU}mcJU}v%HMt^HLPfUDy(iU6N`7aW_ zscmqv4E;{kp{Yrq;(O*6;0aPx@Dz~A*Fc&psC)KbnLVcC8wr*rFMN#>^s=`s6P{ZD z^Efs@qDxSX36|W>dM9K0WwaSR;Y1xh=fTmRy z%VXhG1$I3p1`3EB+uPe=VPUaB-P!wh$8)B5Tq|}h0~#rk`wPECqX3AA^VgPdd3kxM z^ug=%VXSM<9l*eIx__{q9Ec#$iMPWAU^2vK*oXoDuoyh9qHkJd)kTdBV`i)O)_s28 zM>P7O%pM*d0C^;Uz%e!DTr;)Tc;B6%biG3=DQYU1iTqq`L`c@k&b-6YH#Ic{2G`Ni z(bs#T-!CpMXf*46EiNRMI-LjO%f_3Rex7%_#QObTW7DIVLN4RHkB^Vtp_mYsAea)b z%L`Jz#|P_)FYzTM?0WW_ZE%>h+{&!PCcKdmUfibRSn4L~v4I1q%0Jxi-iZhayQin= zd3d}4h!CI^ZftaN%cEhZz}LM#Jrxub7#kaRhr?qt2ENq$*IofU$~-TC=DWQKa2u_t z><=gL3|GFN|apr|zj%HP&KBgHLt1z2e%iTt=I*qOsmWsg#rK zT8OWG)bqzE$n==XzMPw&mT)Be3iQdGmtS<1Ea!O=1)JoRl$6dDDq3O4&AvHVis+;% z3(QAT(nqOEoG8PeII&@gdjZ1rfcQLI9hnj)j(R=KBUNz5F0Ki8y5q674jWhhk7|G- zr(_zUvY-;fxdC>9M8Yq#XU{?H&pDC5(dK(q!^pi;D2?+FkN+yuW?rLS`*OTfA!TeLq>Q!&;#Ui28oFYNshb z>>VdiN+d-^Md>niG|i$*^~ZO-KFD~!-d)J3dh+SIhxqt6h~8DwVpUy_ceQPRld=6$ zwrE9~e?!JWP*R%fq)}w6;#tcViAd}Ecp1qqf_T=JcXRWOo^Cv4{(5(S+dHO#G;%(? zh%`DjwoRXql=b(w7}8F)>0hlS=ue%yi_ls^D?(=fjpJ&D7Qruxl=g6t;xr7T5i`?o zF}~r_RlUd3kTR#Yi>?P~lF(Ri=J2hGrJ$6Bf`vQRuJ!5VV9kPs%2VPcX)v2M{uCER1mRBI=Ah&tMk+n`^4NpCOAh&V(d^r5NhBRX63%5EOoq4q7LEh+D%Zz^;pI;yP zO?h7pQ{L%-yd5i@B5%(6~J6I)4XZa8wOaxoNp)DP$8No^!j zdGfuLM;Q9>t+?_zKtPilVLr6&I3GG@aN0rWE z6-*s8jVy}O{EEHQcDkAvPNm!&{&a&_-qD6`yff(e4sP%;a8u7W_H_mn0koGaLJivy z8JU;m%`8XtY^~+120`AWHo?>i`f^d?c{dYP88uis&GD&sOQAAFM0SePRzv>Gkq&p8 zH1Eq-{>xAAz=AsvBU~|bC+nQ@A zkjyeWmWILa2oKEJ+iv5CuOz_W!$dwF?nNdLKotLp!N&)9F zw@qR7P!WbjSW!BTWOg&GiR3>yVb`q!v0M}tTsIf znuhXayP+s6G#&{5?Xax50OYH`%XrN0cVaC1SQtp5gR zjY9ohUEAWXR9^X&!PgQW_%qb^N|S#L@VwaGEpRDu@$ey}LWeQOJxB1vMAdc|%JxU` zSlqg*a{iiW{I`!$rH~aHd|dsd8Dtq5c**(~pYOcsWnSqn$k)j+Q5$)fjT;2Y+*BC) zxVg8wI(cQTBOjo0C}b z+iTM$H!7=XsD&m5Q82oOJ@fgl{!aw4o>vyzk(&BX?WRF!g;HgJ4&f;;N70SmEI77A zsEk#aR<3UuT)a$%-uNqP0m)dBgOTu`GZmP#q_i)^sk@SJ-1m@2y^)`uFunCs#fG=( z`C!553go=guUDW{Dg`>6*cn>y89@O>6{Z8L%>lv?yEh+N#Q#JI!N{qNcgjp4z?WC*liw-o8Wc>gjR#b%P#ppz7$xXV4c$B z^Ovk%8}#g4G|}AER691MnhDrF3qOy$xK!V>Lj2mj;4pfhD=)*{z}Q)mK6jtGG!k!; zvz3YzM8n z?lQTFmx9?%G}Dp2ukMl@VK^4PEjy=i^K>Uh|I_3viy>&I*BCA1C%{Mra9+)eROvmJ zoMUdnTgdR~I) z%kkPpTx%0ZY?E&*S8d2@N^q*IHqKFQqxw^DQkr${z_^u4wJH<;23O!CUOnps-2PNU z23Epv-m8LTE`3$sJ?D80SJqSwm#usmu+Tf#Y;E)PRXh$qAnMAIEqjz}xrs{Rk68|< zh8cylQrPdfyxAqdri2DjD4@utJ7uw(80wk*E@qRv=oF@j3X}b;y*^N?dEJsw8hlXO zh@4CW!|3pK4p~fV+^XH1_>MVB?cv>U<@5Em0RuLO8W8Ga(F;*$0x?UqoRdqkmC<)}yi7=9iMMZe(j@S%F zg)dnn6GLNTmjo%{A|=DZN1KkIPWMfQ#9NSSPc8GD(+jb?3gfz}Dpe0G-Wv5}g?LHE z{v!nxE>4Gi9YVZV&PHhVvQ7;+Cj2}l%5dHIsV&Ar) zD--|*#jcf}hF$X)qgiR2m*2=QM=oiRf$GoA=W-irGkNqMJ)Xxr_z?eGd)F_J@&fWb zp<<{ee01IB+;(6#J8@WF7nv;c2hAf}PjF^Z`>Al8zDC6+MUmmq$qNX4sVcz@M=_55 zVgUK#vChK8%+wb;Ec5R3QC(c#cJjCU#QoFVNkFr2sK&yi-`cO<+)wF}A8NSKDJE84 zR0{Wm_}9tY<`*jZA3q$cQvwAAzgM^=-tq5z#rQ&EW%u~jc`3IYzmFl1OV4BTHRCj# zJX)g-zBy~Cn_=6ar*h|2AtO24U0GFi)VNVTM35ofzZ^sj`3|e1UbM?n=lr6LRK(`mG-d%w6BfgfCPG(* z#pJ{$VmmY>H!Ae__^`_mG^xDM(&@z>uf?~?WG071##JpoCqMD+y*wtLHWr}Wnz^J7 zE7${>iGCF(Kf&@wLZm)Ax^o5aekU zPl9XGllH(lJA8K^68cnXb$xwq!G^oUz3aIlth{Qvjm;q;#|?DB?tHp@C+se_!n8d0 zuN#`g00(dCezwQ&3`MwcycDF-`t{MTp|YvZ!4-u(WlsBk>NpOj3t`~hoSKYzrqLf; z_n^ze!!;IL?BauG=Yp8tNj)qcB4~}71;?$blaB2{9bx&oFf_MeSdx;%_f_tAcTVg= zsd5)1l5UY-JKbKHP9)9Ftp*{tn4Z{#EJ0Q75dU2p@#C^IA||KGFZpS5<2^HtumaRV zvrttjQD@q04(bcpqoY+0myg?Q&NBIQ=UvrOxJWFNNNgGWKCFDY6V8uIcE`!RkH)LH zi7|tBvd?G^`-V++BO*DtQHanHr{nFz!)OS8l29#h=H^JQWnw`FfmR}ERdPr+ zXJ(U~i7-FbqJ48k}d%Q%D}o zjEHi*Se|c_u$vp=kp>mlVyDBmIDHGGE&gHl0Q-x7bZ=hpM?B*c_ms(-u!V;jHD|wk zE>W`hdUM6yEeZ&B$%D?B-Br{OoNsrD7 z1*0ar4K8);qT}e%BwK#p-5;HkwqdlHTyva8S0TAu{i=#?m+arFV=5dF^~7vripbgb z^#udPrQ9PKDuBFkXD}YCZm%rjrYw=y#hFI6R+Oq2t$0_m86L1cbNo=v0`JA>|RPcT(Nr>yV8 zes$moX4RqGGSRI-#K_`YP1eP|0|K)!cGS=^sRoa}AKb#XVd%Rq*>#^KGa|M3== z9Aku88O=KVUC4%okdsocpZ`0`Tx3dlleiGJoIJH08qD|U5tT^<8ZS@S;(b2Ck~%P1 zsWzQt4yk?`=!JBw@$b9QdEe0d{6NOpWFjaKCr~W$;(wZsEwiBF4D+i2dkxoGnN<^3 z=uuG)MR4-mi$zolUCLD}h@#;9M&#`L3klvfHD)!NGJ#bXT;p#xJfEkE3WJjlmMi7e}Lmt~rvX1X!y5ry?8J z*jDHUZ2Hs7PSDmM9V!m*STd6*-|m^ku8i@e2?028735A(1d7qIzX|ys*w3-l6R6+Q z)4+=1@|OtXoVgU33wkZF{@McTX4aTI}UmW_dZVFM`K&mc>*d$0=^rcrYpa zBDxrF<<%!59mQ^|L+?Rsam(14}{yVF;sB|t0O}$wrRJkNL;rlg(bpFq+eAi^P<{3u5qL0gDj0_XmWGP zE2MYsin?~jTXduz_hpVwO#z^Nca-Kjbf#*IqEk9Lyz|v!kl#1&4~PaE>TEyp#_^ET=JlC7IC=imM?GhH_|c)Zv-_uu1@t}k z5`u&U5{1|CT@@x*h~un@YMv9qjS1t+^L^bSV?`b~-Wj-{0rt!7RZ$i#^8kev?%YsDm*IT@AVa^bFAcsU0b1UIw_SK_rJ97k*tunft*8puq*F;}Hen50$ zXsCFbnIwfyFIe>9V!f^Zh6Q=8LDZTg{tuajwHpm?AO>vbd^lY|ypN`42{p~A;-OhG zI(JQ8?ViZp`r2S(6=$iDF@k&_MZ_S9gwandf#J)p&rbJC%}&7u;X6}>o@lS`ZES*Rd;K4_&pGQ3-iNKq;fEc2 z<#seq3L5RKxot9Rg&H8FY_w~NR*JQ;1mG7_s!!(s2&W9kPIr#5+Wp9g`PloX=z z5BmbF>q2Hn{<+(+5_Y0{gpux2#-4W%Pv#Z$-^V{s>iak6P-FCC3}XzGI`%?OIHy++ ziey9Z)bVN!3EkwEy6Y#%9zH!-3`rSVm*Hk%8^UXG*&cN!g7IoRIt>c>J9MDjS4aFm zIm+4X9oqdM+0B<51e2XC7$RqU=*tUzDj;dR9{x7lD~g}`SA(iW>ARve8H?20{KBw* zXHWvq)DFRkCmxG)Fhl#`VsyR~Rkbg*y&B5X>0zA_BCLxeLwfUI(H=WnKZl?QS~40+ z)X%B&pgn?+FL#p%my`Q^+PAPqIg%9Pb-n4<%8)aRl33yYQ5xjxsFf0dcdCf;*X!dB z!wv9{K2Ya5mG>IEIa{v76>9gTc7Ai;0TW87>m-&5&V`_^A(5J1S^HW#CBpV8pj;fHDaZX3ySEC3{h0NHM95 zKi~)Kq?UHyNX6jG>vXsXozAaW=Xtfu`<@^1v@NZ!J+vt<81L&tpnR&DG4qC|1G&nruJ*BPj~4Yt?u#*d*UM(fH!lKr;dF;7JyEub|s*PLL#7AO*Z zAsbF~849FtXH@BN2$mb!2hGB|@+{;mkW_Jl5Le7bX7hH1+le3EC(_}T&L=4YZD3WX z1f?3YcMBxEaEKD7h7vWt3TG83I=q&Ct)$$;f$L5QB`JcKwmfiiySCC}OVH>5Nj&K` zlZuy4j;6Iqy1_%X(g?n?^kXL54|5g-@89s{!T5P~dL_sCX;Z+0uy|jMK=4QY?2t_R4ZD(}J%;bw}O@($5{ZAGFMqFhD z?*z~cYt{R5s%32x=_wX4zTqY&ABu^sn2ctI*OG|uIsTKUvBfejz^rYA44nuRBnXYh|bJPAt!zl*bFS0>TztDOs=#yezVp zsA)qVqt$s0CncH!iPDI4M-FC0%O56Qjb}FY(yhNo<*xAgJmhzjHe^VqS$?)JM|hX@ zDD!i$P_qvCS93m7w9vw)El~?z0$^aeTa)qLaNJz692sfevZDzyDt4xxD$}qW&&$~I zpPLi1c^eb8zS07`_vfEQonnF!$YsmQfy^7b6$f0J%;jHMO8uRn7}=%x`KO^L9MK2$ zbf3ln1sq^D)4h9bv({{Pvo~_^o5$An(qgWtrL8SupsIdKOhZG%-u?zq)d4EJMUdC; zq?4xw;Q1+G3xKo}^6{%Tm;K}@?~iW3zAJD#lCEnFxWP+7%EJm=L9i=~ma_!U9xBtJ zD@47kaLcOJ0~j3I4r1apK!9}bUslikV&>cZ1%<(T3z#(x3=Ali-p$GJ$w*6kzT8<% zz`7mJ7N&=n0j?Yn5D+Y=Dpk52tTt<*NL=nB{6BPcb-%Ixe0sZ``kkMDlDn4VB745< zb08ay$^bR|5+sLxK8`z@8?M>LI#hw=bZ@RGOYvs2$;AAY#k;bL6VBuv{`R5h&g-zr zCSM?GQgp7dQA5x)pzYSLErlBrBnI9(j7-_jFsJ6z1;A1cbcr6i+MZ|og0O6Vz1=+Xu9&UaaTK8NW(b1v{p9gcHU=XN!eMU zZ>^;TS#O*B?aW<;`7k$*C&qr)x{{xUJl|3nGntQ&RWX$s+bh9%vCXS}!%lMe`HEpp zdHzOJ6m5q(C+*y+U5>-9=)7G#cRaxOTj-*vNDaf_BTK0bDYNBOIBWnhX3Kn|11Zb( zu9afqGUAOl`?s5aD|l6-mioWD)m<21ZY@@5nK2$coUfLvF%9`qp@k#yKINv78y}3P zvGkrdyIyuYo`^oRYmI0K4jJtB4kBDuPKHLT;SHq|DVK~&@uJ-F&u~$5pCQPd-bwM< za)pOTs&siaTqwF5#N;LP4I!xx4Q(h8*VJeQP)2Li{X-&h|9>Q6(3A-haVRtI2#f)# z47qLt`3BDeg0uT`VdHzx_wPcjUkFgc%_n{rK%wSTL%WXcTs*(8%I-9U$JjnGl2UXH zZCx7}y@bu}vQ~1JFryv}ub6b7SJUfM)5o~7qZ^;&;iBTcC1Vy4p%ROe;3>ikm7cv` zC}Tu#KfRzcEZ(&3Dl-x$=`AUj2u{pNK{@wR>IB1oV?1Kf67q?uh9_#Dijt>d9qn*? z&+E2ZQdhk_&V6?E%$b?8o0hX>Espf=eC^P_y;i^7g-oeU$Apy#Pp-RKrIHOqj{ZhZh%Jj;8atoX?tlKD`sv zA|fKBqK1}>WWG^RmE*Yh;Pm~6l+&`kkzQX_pIi2?Rw>9;l5Xp)IKkghEvrT)W&DIozn?#;y9{Ze= zn@Rs`9vbA68IdyW=6Ay~#)1_816YSO@l@h2k}A(frxc-2BgN~#MT&}%gh?cGVK8D)BQRoE-MlG75z^i>ziv11)hYORK->RAPQ=UI z3`9pK`?VzZ#)Nw&KsEWwS?y8TsAi= z)9`SGL0qQ~e{l0g6r5f3y5FEqS>);Wuq^=LGpJn{#2AwPj-n?F6GgT=gG1=I)_{hl zhW}#^E>66=q=P;MyR^N zUrJ7nZJAWi4Cm3SuC7jIQXaAotZ{T>LzgIypF8#l@J<->i1ze81Tuxw3HWtD>>A-t zj+MD@um7xa`8RWdMwiPCfcXG?XJN7!Us-7d#EASE_}s2nLj*qD-fzvo)r=Fr`*^*K zA>hNWx5jlpy1%;>sm=XRqS$+thS|x~S^Jb@ zdC`k()H(}NMA7q&iTV^Q>eFqj7Cq05DxA0M(i!uhoGpUniDoQC7evXfiGZ(o4Cjrw^jJN%8 zJUl$FyYDFwnG5A(!Cb4^=ji^JuY^3;JOWsKM^B00~$dAD|(|r z_$Suu2|x<-r6XZ%f0`GU%QwVsYE{>J14@oip*7s$Qy_j2tn6>X728Se#O% zg#6w)PmT@Sp=<9UO7QEOwDCG*-dIZ2p@#t-4j|zZWZ0kgV=^9X^t&o5O^!8e#*~4f zT$*DPx*2L8PGOLi+M1usNg6Ufr8Eg@iO;pif7a_`%HA%t?kj2rm+MnGV@rl&@Z z2Be7NwU)XbFDWUh@pIpEKQ}~ZXpjH|jEe}mPWt2cHWXa8(ZJQZaDl&j$Fjx!`iy+%Sn_qod)c*AS(ZI&B!3n2SJP2SiUW zjsSl^Lc@vZZwaX~Z>&2Gz76dsDR>_M%VgLOUGiQ2TqYbUtWvuLb?Fd}36uw6%Ncm0 z*E>Ae`eCZ5HS3`{v7F?8nF{*FsPdF?xm@6~{{k_7xViDi0P*gf0GLX?lX+r`z~f*l zb(uTE?s~OF*YyVaE)53+DY52mBvf7^MF20sGhbxd$dHDya5h z&gYw%myB-kpw;`!eJA*v)}{gK*sE^HtAi z*O+h|h&^|!oa9NPrAo!1N&SV zVT%f`+zj1|6-#qbemOVd{IqvsM^z--z8(d z|HyjrH%h$o-KB{SZJeo>M&zeBEft%YU>@&o1?ii^SX+UYdh3KT}47Y`Gz*S+0fwNT4%A`xTiulNtp zT0+r#$huK8|ABTaHlii-a<~)~d7x&+LMNqIt3)D&zcDUU;=v|n7_VDnwkm&w_n>wT z7z6E(CUsi?;{Q-Iy3#sO?k0+D14QuQ{c$rR44hMtokLqeK>P{W zm{9|eP$3fV!Uw{@fBsX1$Yih@^n&Z@>6JWB2Uz-wIA3n?=r48iw&;>?xm`6ZP`ey2 z*B!#;ghfX~vg%!Cr2(mN{5+SG-^=#^ItB!!L+I>^jd?wFW1F?wyVZ;+1?INPU8``G z&`*0(ItIq=TgdPr^?Um1Pgsq5QsLq+ol#RERBh$T5ICCo<_gCzNRoSuAG<1-Xyvfk z#19-R#^XH^3*Z^<>#br(DY`-`Q!J=8nPM|W$rYv2_?q)JcjNoNC6%Y<#e4t3OORRv zk@QB}X++4uBb45YgItb)Mh`*+CoE9H#BskRSb5NMxIhu46#9oDLyn1FZMZ)+=P{+J zD#jZ{g2n*|bKF?-bGaZ#Iuf3>W9A zZCa?Cl+;u#WMp3pqEy&jWx%R`VD~ycKHe7;2YmbR`!P^#1}OBqwDDm~9 z57j9E?XLu+WVy_QSwk@dLyy}M$b&$s3K(;Qu8X3x4aqc9&4HMG5fv8p)j#lZtpWDk zpXK5Dp_BahfyI91J3)m8TdI7rvje&bjSt;-Gzh9Egz3-aXiho|V%dQ60+uw>_5)+E zVcob8;^>r#9wfF=rCb&H`DTAi0Ln(&o>@>L& zwd{xtv8Q?lA9M&)zJ)WSPEEoYl}8^~d`(|82R{e;1|1agC&}sh?EyI9VVC>9HamJ-)M#ravPkDGA)x zfE(%A{{BC9w4nl{l#+kSuJxy@qV%V#u$Byzr5pYUOezX@0i_jL0YiWlEA4$ z7W>cQ>t)wembB_)H?i1sru9rM%M+tyM27j;q~H}Yv;-9v(fGr zi@c`Y?rLXfNF*Qle0yj_Brb3EW=|DS){c08K3}TT9mvhi4d03&pW^-Te7Llv9#9E- z)p*E>;wi)uMWMcdqygbN+(UDzl+I1V-ILe}hQ0~B}&&Yzf1)4x8$U!McQ zkxyfO4to|plRKr2%yahAK|GtT<;DoZgE8Z?Ov)e z0xGz7)LPZ&>Df0FzOCqLK-u=JjFK zR7TIL~*%GA|j$YB%nytSdy0`1hs+Eq3f6J zo!k7mSczB0&ePQC4tEc#Z|}PC{ptol3uQ9dQ9g*AC2$QxY_79Vh((dz#rXM;bRgKk zQoDg%uz1C+0 zAWSQd3{!6bwX^HVcDEmx8#n%vl$e;9=O#Z^GticHG}gOvd-=5H=7J!N32Ht`#DX7+ z(557$>zJ(}UI`K-kO2BSt2LE|>T00fS%rI?J8*V};KUk_V)_~=RH#UCm71GNSfWqr zGVc977w)>L>m9-47+9|yrk1rU!YvdG?+05tV}5l&J)MKi>&dSD&kk&QAAoQA5-F5$ zOiK-(QBHGvF{crmTNt2t)?1y%j8T|FM%(T8CC9J$X}S-q`D}qO);!_I^^3sLmZvh4 zgpja=^ThCCJ%LOPDA49!oL0U&oGgPoQrm@OiU~z@W+h{To0v$A_4J zKFfUTkB0tY&@~m%wCm60WoRwa(gSrraILer-EJ~$J7FrwnA7zIeO&~>U`Z3GBJ1jI z3nUVJ#{~p|PTy6V8t_}=;J{YTk}UKOcm8*m5_Z$f0{ z!DlcG{J`mq11dgM){!qYQU_$RnW1fpFLV&@N=tU$&o?H!YUv}{8{LQA@AqoCc2n6r zeNt4vZ1;eV+hbIl+xoI{9qsFz+cU#;v<%i{1)cCtbvrJwir`Ge$47vrqEfW;h3jm- z^ed_JG}^}@-zNk9ZSU&3PV=uGX>NY1o|DJ(Rm^k+{ezN!V=Z^+l1Cl(YnH z?`pOu7}1x}B&rZ3fkpN1Ie={}Xif4v?e_>KwaHkz8601pQqcSoSm`sD)vuH6s0 zg5K{ebyA?!-3EQZ0u9==#xTDqZi^o}IGg)|e(?Z^RcI*m5m%ZvG&c}tfyRv3xhU5g zl3qN5!X7AnnX3Mh!enNj158e(8a_f&G-8R#YPSDfv#fSub?5~V_?Vu3DknBbc`DIXz4g@e*WOh{<%K5fpd)no$^52YOA z@d>ooFO!Pm;^F`vhQgJfm6^GQJD<;YSqT}JlyqO4A^qGSCUxF^Gr-F6_m7A}Smc)Pyb9g}VXuV=S=-PM6&7ZpirodIO(2~~KmdIDJK$mQ&4tbfa#na_JE_3|6Q@HFuW+W!>y}rezRA73 z;<~NDGZ(Jnzu{Pp?sxPP6mzU{tBILc^!*&2us2hG`J{$)&E2vRd55xlJnY|oJ~YO^ zlIwec$r5-}UL1Aj=VtR>{Om>__n)o94q~n;o`RRi*W4y%f(F|m6Re@ebBQC)533Y> zNCx^UV@0pQZ(WKACjsOrB35OqHWB2?nr+1`zJcPQ=1%uYEiT3>s(rPd^OgRsa*xjc9&*rygZZ`f5(Co#+)TjX3eie* z(P}HPn-y|bmZj6^XUqNL*f}Q& zGCf-<<-;t(Zh-|0&7U}LZxdxOLqq>@d|P>d058^;OWRp$7EWPcqET3iDXfdl)@zG3 z^iz~(uqG{3Z|~zcUqn35Rlu7HyL3@c@7;Esg9<9wrKD1H>3{w(bA9c=apFI2B_B=J zPQkBg&1THu8EjmWgJgyu>TE*$-!zwYn%^_F(F1o5C!?DlU~)dAzvTl% zu4`*}E>kk=L(Gp4bF%Z1y!7R7y z)rPjY+U$Uq%Pj&N{2i;4tNV5%kH!IQgUuv%M7Wrv!*MVKf=DxF?#0C5?-CGB@2g2# zbJ@zVdgVUU3M1ze5lzc7nz*wn!4Y(p6NfHbU60$-MoEN=@m1F>Vni*Bhha|vU}Mi(PVB<~Nw!Guu#MDCO+Qk?yrs08jO z9d~|L#Gh?X8skl_eYnC59MX>XYbhZA3&;jFzis#S1lwVy#$PeD`Yiv|V zjM50`_}U$`9|8uC2Ebeofi^0p9J(lUGUfkx*j<4x!Qh@ba2O4R!^?@*Ov9bjpFc@-T(fbG_%ESa?>QW9kfwNd%A{;?_yoIqr&g5LQTQE2m1 zws3_HWonHj?7sd>B7o3{9ZF!XSiYbc1{xYTMK#><4)`gEh%neU*C0^@{cruV0eGj0(2Y3KnEx|~K@AHuzou2z*v;2yf(c|YtdR9r5fXF-xda4E zcp0`n1~&K?26&V>0(}7j7j7^v;aefw`utv|$t>Uq!lg?=n|AcTj3{J$0X!dGywl~t f+Xv78a~m++%G<7Ha_I9*1|aZs^>bP0l+XkKG2Q3) literal 0 HcmV?d00001 diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 937e3895ac..373683fd76 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -5,6 +5,8 @@ $body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida $sans-serif: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; $body-font-size: 14px; $body-line-height: 20px; +$blue: #5597dd; + $light-blue: #f0f7fd; $dark-blue: #50545c; @@ -28,128 +30,1007 @@ $error-red: rgb(253, 87, 87); text-shadow: none; } -// Base html styles -html { - height: 100%; + +// ------------------------------------- +// +// Universal +// +// ------------------------------------- + +body { + min-width: 980px; + background: #f3f4f5; + font-family: 'Open Sans', sans-serif; + font-size: 16px; + line-height: 1.6; + color: #3c3c3c; } a { - text-decoration: none; - color: #888; - @include transition; + text-decoration: none; + color: $blue; + -webkit-transition: color .15s; - &:hover { - color: #ccc; - } + &:hover { + color: #cb9c40; + } } -input { - font-family: $body-font-family; +h1 { + font-size: 28px; + margin: 36px 6px; } -button, input[type="submit"], .button { - background-color: $orange; - border: 1px solid darken($orange, 15%); - @include border-radius(4px); - @include box-shadow(inset 0 0 0 1px adjust-hue($orange, 20%), 0 1px 0 #fff); - color: #fff; - font-weight: bold; - @include linear-gradient(adjust-hue($orange, 8%), $orange); - padding: 6px 20px; - text-shadow: 0 1px 0 darken($orange, 10%); - -webkit-font-smoothing: antialiased; - - &:hover, &:focus { - @include box-shadow(inset 0 0 6px 1px adjust-hue($orange, 30%)); - } +.main-wrapper { + position: relative; + margin: 0 40px; } -#{$all-text-inputs}, textarea { - border: 1px solid $dark-blue; - @include box-shadow(inset 0 3px 6px $light-blue); - color: lighten($dark-blue, 30%); - font: $body-font-size $body-font-family; - outline: none; - padding: 4px 6px; - - &:hover { - background: lighten($yellow, 13%); - color: $dark-blue; - } - - &:focus { - @include box-shadow(inset 0 3px 6px $light-blue, 0 0 3px lighten($bright-blue, 10%)); - color: $dark-blue; - background: lighten($yellow, 13%); - } +.inner-wrapper { + position: relative; + max-width: 1280px; + margin: auto; } -textarea { - @include box-sizing(border-box); - display: block; - line-height: lh(); - padding: 15px; - width: 100%; +.window { + background: #fff; + border: 1px solid #8891a1; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); } -// Extends -.new-module { - position: relative; - - a { - padding: 6px; - display: block; - } - - ul.new-dropdown { - list-style: none; - position: absolute; - - li { - display: none; - padding: 6px; - } - - } - - &:hover { - ul.new-dropdown { - display: block; - } - } +.sidebar { + float: right; + width: 28%; } -.draggable { - background: url('../img/drag-handle.png') no-repeat center; - text-indent: -9999px; - display: block; - cursor: move; - height: 100%; - padding: 0; - @include position(absolute, 0px 0px 0 0); - width: 30px; - z-index: 99; +footer { + clear: both; + height: 100px; } -.editable { - &:hover { - background: lighten($yellow, 10%); - } - - button { - padding: 4px 10px; - } +@mixin clearfix { + &:after { + content: ''; + display: block; + height: 0; + visibility: hidden; + clear: both; + } } -.wip { - outline: 1px solid #f00 !important; - position: relative; - &:after { - content: "WIP"; - font-size: 8px; - padding: 2px; - background: #f00; - color: #fff; - @include position(absolute, 0px 0px 0 0); - } +input[type="text"] { + padding: 6px 8px 8px; + box-sizing: border-box; + border: 1px solid #b0b6c2; + border-radius: 2px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)) #edf1f5; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1) inset; + font-family: 'Open Sans', sans-serif; + font-size: 11px; + color: #3c3c3c; + outline: 0; + + &::-webkit-input-placeholder { + color: #979faf; + } } + +input.search { + padding: 6px 15px 8px 30px; + box-sizing: border-box; + border: 1px solid #b0b6c2; + border-radius: 20px; + background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; + font-family: 'Open Sans', sans-serif; + color: #3c3c3c; + outline: 0; + + &::-webkit-input-placeholder { + color: #979faf; + } +} + +label { + font-size: 12px; +} + +@mixin blue-button { + display: inline-block; + padding: 4px 20px 6px; + border: 1px solid #437fbf; + border-radius: 3px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) $blue; + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + font-size: 14px; + font-weight: 700; + color: #fff; +} + +@mixin white-button { + display: inline-block; + padding: 4px 20px 6px; + border: 1px solid #8891a1; + border-radius: 3px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 60%) #dfe5eb; + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + font-size: 14px; + font-weight: 700; + color: #8891a1; +} + +@mixin orange-button { + display: inline-block; + padding: 4px 20px 6px; + border: 1px solid #3c3c3c; + border-radius: 3px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%) #edbd3c; + box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + font-size: 14px; + font-weight: 700; + color: #3c3c3c; +} + + + + + +// ------------------------------------- +// +// Courseware +// +// ------------------------------------- + +input.courseware-unit-search-input { + position: absolute; + right: 0; + top: 5px; + width: 260px; + background-color: #fff; +} + +.courseware-overview { + .new-courseware-section-button { + display: block; + padding: 12px 0; + border-radius: 3px; + border: 1px solid #8891a1; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3; + font-size: 14px; + font-weight: 700; + text-align: center; + color: #6d788b; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + } +} + +.courseware-section { + position: relative; + background: #fff; + border: 1px solid #8891a1; + border-radius: 3px; + margin: 10px 0; + padding-bottom: 12px; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + + &:first-child { + margin-top: 0; + } + + &.collapsed { + padding-bottom: 0; + + header { + height: 47px; + } + + h4 { + display: none; + } + } + + &.collapsed > ol, + .collapsed > ol { + display: none; + } + + .publish-date { + position: absolute; + left: -60px; + top: 0; + width: 40px; + height: 40px; + background: url(../img/date-circle.png) no-repeat; + font-size: 12px; + text-align: center; + + .month, + .day { + position: absolute; + display: block; + width: 40px; + line-height: 0; + } + + .month { + top: 10px; + font-size: 9px; + font-weight: 700; + color: #f3f4f5; + } + + .day { + top: 25px; + font-size: 16px; + font-weight: 700; + color: #b0b6c2; + } + } + + header { + height: 67px; + + .item-actions { + margin-top: 11px; + margin-right: 12px; + + .edit-button, + .delete-button { + margin-top: -3px; + } + } + + .expand-collapse-icon { + float: left; + margin: 16px 6px 16px 16px; + } + + .drag-handle { + margin-left: 19px; + } + } + + h3 { + font-size: 16px; + font-weight: 700; + color: $blue; + } + + h4 { + font-size: 12px; + color: #878e9d; + + strong { + font-weight: 700; + } + } + + > ol { + margin: 0 12px; + border: 1px solid #ced2db; + } + + ol { + .section-item { + display: block; + padding: 6px 8px 8px 16px; + background: #edf1f5; + font-size: 13px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.new-unit-item { + font-weight: 700; + color: #6d788b; + + &:hover { + background: #d1dae3; + } + } + + .draft-item { + color: #a4aab7; + } + + .item-actions { + display: none; + } + } + + a { + color: #2c2e33; + } + } + + ol ol { + .section-item { + padding-left: 56px; + } + } + + ol ol ol { + .section-item { + padding-left: 96px; + } + } +} + +.item-actions { + float: right; + + .edit-button, + .delete-button { + float: left; + margin-right: 6px; + color: #a4aab7; + } +} + +.item-details { + float: left; + padding: 10px 0; +} + + +.unit-library { + float: right; + width: 28%; + padding-bottom: 12px; + border: 1px solid #8891a1; + border-radius: 3px; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + + h2 { + font-weight: 700; + margin: 14px; + } + + .list-search { + margin: 0 12px 8px; + } + + .list-search-input { + width: 100%; + padding: 6px 15px 8px 30px; + box-sizing: border-box; + border: 1px solid #b0b6c2; + border-radius: 20px; + background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; + font-family: 'Open Sans', sans-serif; + color: #3c3c3c; + outline: 0; + + &::-webkit-input-placeholder { + color: #979faf; + } + } + + .list-wrapper { + height: 500px; + margin: 0 12px; + border: 1px solid #ced2db; + overflow-y: scroll; + } + + ul { + .section-item { + display: block; + padding: 6px 8px 8px 10px; + background: #edf1f5; + font-size: 13px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.new-unit-item { + font-weight: 700; + color: #6d788b; + + &:hover { + background: #d1dae3; + } + } + + .item-actions { + display: none; + } + } + + a { + color: #2c2e33; + } + } +} + + + + + +// ------------------------------------- +// +// Unit Editor +// +// ------------------------------------- + +.unit .main-wrapper { + margin: 40px; +} + +.unit-body { + float: left; + width: 70%; + + .breadcrumbs { + border-radius: 3px 3px 0 0; + border-bottom: 1px solid #cbd1db; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%) #edf1f5; + box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset; + @include clearfix; + + li { + float: left; + } + + a, + .current-page { + display: block; + padding: 15px 35px 15px 30px; + font-size: 14px; + background: url(../img/breadcrumb-arrow.png) no-repeat right center; + } + } + + h2 { + margin: 30px 40px; + color: #646464; + font-size: 19px; + font-weight: 300; + letter-spacing: 1px; + text-transform: uppercase; + } + + .components { + > li { + position: relative; + z-index: 10; + margin: 20px 40px; + border: 1px solid #d1ddec; + border-radius: 3px; + background: #fff; + -webkit-transition: border-color .15s; + + &:hover { + border-color: #6696d7; + + .drag-handle, + .component-actions { + opacity: 1; + } + } + + &.editing { + border-color: #6696d7; + + &:hover { + .drag-handle, + .component-actions { + opacity: 0; + } + } + } + + .rendered-component { + padding: 20px; + } + + .component-actions { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + -webkit-transition: opacity .15s; + } + + .edit-button, + .delete-button { + float: left; + padding: 3px 10px 4px; + margin-left: 3px; + border: 1px solid #fff; + border-radius: 3px; + background: $blue; + font-size: 12px; + color: #fff; + -webkit-transition: all .15s; + + &:hover { + background-color: $blue; + color: #fff; + } + + .edit-icon, + .delete-icon { + margin-right: 4px; + } + } + + .drag-handle { + position: absolute; + display: block; + top: -1px; + right: -16px; + z-index: -1; + width: 15px; + height: 100%; + border-radius: 0 3px 3px 0; + border: 1px solid $blue; + background: url(../img/drag-handles.png) center no-repeat $blue; + cursor: move; + opacity: 0; + -webkit-transition: opacity .15s; + } + + &.new-component-item { + padding: 0; + border: 1px solid #8891a1; + border-radius: 3px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3; + box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; + -webkit-transition: background-color .15s, border-color .15s; + + &.adding { + background-color: $blue; + border-color: #437fbf; + } + + .new-component-button { + display: block; + padding: 20px; + text-align: center; + color: #6d788b; + } + + h5 { + margin-bottom: 8px; + color: #fff; + font-weight: 700; + } + + .rendered-component { + display: none; + background: #fff; + border-radius: 3px 3px 0 0; + } + + .new-component-type { + @include clearfix; + + a { + position: relative; + float: left; + width: 100px; + height: 100px; + margin-right: 10px; + border-radius: 8px; + font-size: 13px; + line-height: 14px; + color: #fff; + text-align: center; + box-shadow: 0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset; + -webkit-transition: background-color .15s; + + &:hover { + background-color: rgba(255, 255, 255, .2); + } + + .name { + position: absolute; + bottom: 5px; + left: 0; + width: 100%; + padding: 10px; + box-sizing: border-box; + } + } + } + + .new-component-step-1, + .new-component-step-2 { + display: none; + padding: 20px; + } + } + } + + .video-unit img, + .discussion-unit img { + width: 100%; + } + } + + .component-editor, + .new-component-step-2 { + display: none; + padding: 20px; + background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)) $blue; + + h5 { + margin-bottom: 8px; + color: #fff; + font-weight: 700; + } + + textarea { + width: 100%; + min-height: 80px; + padding: 10px; + box-sizing: border-box; + border: 1px solid #3c3c3c; + font-family: Monaco, monospace; + } + + .save-button { + @include orange-button; + margin-right: 8px; + } + + .cancel-button { + @include blue-button; + border-color: #30649c; + } + } +} + +.unit-properties, +.unit-history { + margin-bottom: 20px; + + .window-contents { + padding: 20px; + } + + h4 { + padding: 6px 14px; + border-bottom: 1px solid #cbd1db; + border-radius: 3px 3px 0 0; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%) #edf1f5; + box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset; + font-size: 14px; + font-weight: 600; + } + + .row { + margin-bottom: 15px; + } + + input[type="text"] { + display: block; + width: 100%; + } + + .visibility-options .option { + margin-right: 10px; + padding: 3px 13px 6px; + border-radius: 3px; + background: #edf1f5; + + &.checked { + background: #d1dae3; + } + + input[type="radio"] { + margin-right: 7px; + } + } + + .save-button { + @include blue-button; + margin-top: 10px; + } + + .publish-button { + @include orange-button; + margin-top: 10px; + border-color: #bda046; + } +} + +.unit-properties { + .window-contents { + padding: 10px 20px; + } +} + +.unit-history { + &.collapsed { + h4 { + border-bottom: none; + border-radius: 3px; + } + + .window-contents { + display: none; + } + } + + ol { + border: 1px solid #ced2db; + + li { + display: block; + padding: 6px 8px 8px 10px; + background: #edf1f5; + font-size: 12px; + + &:hover { + background: #fffcf1; + + .item-actions { + display: block; + } + } + + &.checked { + background: #d1dae3; + } + + .item-actions { + display: none; + } + + input[type="radio"] { + margin-right: 7px; + } + } + } +} + + + + + +// ------------------------------------- +// +// Icons / Tags +// +// ------------------------------------- + +.expand-collapse-icon { + position: relative; + display: inline-block; + width: 9px; + height: 11px; + margin-right: 10px; + background: url(../img/expand-collapse-icons.png) no-repeat; + + &.expand { + top: 1px; + background-position: 0 0; + } + + &.collapse { + top: -1px; + background-position: 0 -11px; + } +} + +.sequence-icon { + display: inline-block; + width: 15px; + height: 9px; + margin-right: 5px; + background: url(../img/sequence-icon.png) no-repeat; +} + +.video-icon { + display: inline-block; + width: 14px; + height: 12px; + margin-right: 5px; + background: url(../img/video-icon.png) no-repeat; +} + +.list-icon { + display: inline-block; + width: 14px; + height: 10px; + margin-right: 5px; + background: url(../img/list-icon.png) no-repeat; +} + +.edit-icon { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 2px; + background: url(../img/edit-icon.png) no-repeat; + + &.white { + background: url(../img/edit-icon-white.png) no-repeat; + } +} + +.delete-icon { + display: inline-block; + width: 10px; + height: 11px; + margin-right: 2px; + background: url(../img/delete-icon.png) no-repeat; + + &.white { + background: url(../img/delete-icon-white.png) no-repeat; + } +} + +.drag-handle { + display: inline-block; + float: right; + width: 7px; + height: 22px; + margin-left: 10px; + background: url(../img/drag-handles.png) no-repeat; + cursor: move; +} + +.draft-tag, +.publish-flag { + margin-left: 3px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + color: #a4aab7; +} + +.plus-icon { + display: inline-block; + width: 11px; + height: 11px; + margin-right: 8px; + background: url(../img/plus-icon.png) no-repeat; +} + +.textbook-icon { + display: inline-block; + width: 32px; + height: 32px; + margin-right: 8px; + vertical-align: middle; + background: url(../img/textbook-icon.png) no-repeat; +} + +.slides-icon { + display: inline-block; + width: 32px; + height: 32px; + margin-right: 8px; + vertical-align: middle; + background: url(../img/slides-icon.png) no-repeat; +} + +.large-slide-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-slide-icon.png) center no-repeat; +} + +.large-textbook-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-textbook-icon.png) center no-repeat; +} + +.large-discussion-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-discussion-icon.png) center no-repeat; +} + +.large-freeform-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-freeform-icon.png) center no-repeat; +} + +.large-problem-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-problem-icon.png) center no-repeat; +} + +.large-video-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-video-icon.png) center no-repeat; +} + + + + +// ------------------------------------- +// +// Modal +// +// ------------------------------------- + +.modal-cover { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, .8); +} + +.history-modal { + display: none; + position: fixed; + top: 60px; + left: 50%; + z-index: 1001; + width: 930px; + height: 540px; + margin-left: -465px; + background: #fff; + + .modal-body { + height: 400px; + padding: 40px; + overflow-y: scroll; + } + + .modal-actions { + height: 60px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)) #d1dae3; + } + + h2 { + margin: 0 10px 30px; + color: #646464; + font-size: 19px; + font-weight: 300; + letter-spacing: 1px; + text-transform: uppercase; + } + + p { + margin: 20px; + } + + .revert-button { + @include blue-button; + margin: 13px 6px 0 13px; + } + + .close-button { + @include white-button; + margin-top: 13px; + } +} + + diff --git a/cms/static/sass/_reset.scss b/cms/static/sass/_reset.scss new file mode 100644 index 0000000000..6b4b653e87 --- /dev/null +++ b/cms/static/sass/_reset.scss @@ -0,0 +1,57 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +/* remember to define visible focus styles! +:focus { + outline: ?????; +} */ + +/* remember to highlight inserts somehow! */ +ins { + text-decoration: none; +} +del { + text-decoration: line-through; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 49a51a59fb..f30d8cde3a 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -2,6 +2,7 @@ @import 'vendor/normalize'; @import 'keyframes'; +@import 'reset'; @import 'base', 'layout', 'content-types'; @import 'calendar'; @import 'section', 'unit', 'index'; diff --git a/cms/templates/overview.html b/cms/templates/overview.html new file mode 100644 index 0000000000..cdfbe81917 --- /dev/null +++ b/cms/templates/overview.html @@ -0,0 +1,1002 @@ +<%inherit file="base.html" /> +<%block name="title">CMS Courseware Overview +<%include file="widgets/header.html"/> + +<%block name="content"> +

+
+

Courseware

+ +
+ New Section +
+
+ +
+

Incremental Analysis

+

Unscheduled: click here to set

+
+
+ + + +
+
+
    +
  1. + + New Unit + +
  2. +
+
+
+
+ +
+

Nonlinear Elements

+

Unscheduled: click here to set

+
+
+ + + +
+
+
    + + + + +
  1. + + New Unit + +
  2. +
+
+ + + +
+
+ +
+

Linearity

+

Unscheduled: click here to set

+
+
+ + + +
+
+
    + + + + +
  1. + + New Unit + +
  2. +
+
+ + + + + +
+
+ + +
+ + + +
+
+
    + + + + +
  1. + + New Unit + +
  2. +
+
+
+
+ +
+

Overview

+

Published: 9/18/2012 at 12:00am EST

+
+
+ + + +
+
+
    + + + + +
+
+
+
+
+ + <%include file="widgets/upload_assets.html"/> + +
+ diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index c0b9f9e3af..c21f1f564b 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -3,14 +3,6 @@ - ${editable_preview} +
+ ${editable_preview} +
diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index 12b0d33039..5bfafd46a0 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -3,18 +3,28 @@ -<%def name="enum_units(subsection)"> +<%def name="enum_units(subsection, actions=True, selected=None)">
    % for unit in subsection.get_children():
  1. -
    + <% + if unit.location == selected: + selected_class = 'editing' + else: + selected_class = '' + %> +
  2. % endfor From 5390a19fc067fbb7d85e7b5b29132fe4ffd35634 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 3 Oct 2012 16:19:14 -0400 Subject: [PATCH 0062/1010] delete unit in overview and subsection_edit pages --- cms/djangoapps/contentstore/views.py | 27 ++++++++++++++++++++++----- cms/static/js/base.js | 16 ++++++++++++++++ cms/templates/widgets/units.html | 7 +++++-- cms/urls.py | 1 - 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 1df2c44ff7..78a1a41bc1 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -209,10 +209,6 @@ def preview_component(request, location): }) -@login_required -def delete_unit(request, location): - pass - def user_author_string(user): '''Get an author string for commits by this user. Format: @@ -382,12 +378,33 @@ def get_module_previews(request, descriptor): preview_html.append(module.get_html()) return preview_html +def _delete_item(item, recurse=False): + if recurse: + children = item.get_children() + for child in children: + _delete_item(child) + + modulestore().delete_item(item.location); + @login_required @expect_json def delete_item(request): item_location = request.POST['id'] - modulestore().delete_item(item_location) + + # check permissions for this user within this course + if not has_access(request.user, item_location): + raise PermissionDenied() + + # optional parameter to delete all children (default False) + delete_children = False + if 'delete_children' in request.POST: + delete_children = request.POST['delete_children'] in ['true', 'True'] + + item = modulestore().get_item(item_location) + + _delete_item(item) + return HttpResponse() diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 16a7f87202..86846ee5e4 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -20,8 +20,24 @@ $(document).ready(function() { $('.unit-history ol a').bind('click', showHistoryModal); $modal.bind('click', hideHistoryModal); $modalCover.bind('click', hideHistoryModal); + + $('.unit .item-actions .delete-button').bind('click', deleteUnit); }); +function deleteUnit(e) { + e.preventDefault(); + var id = $(this).data('id'); + var _this = $(this); + + $.post('/delete_item', + {'id': id, 'delete_children' : 'true'}, + function(data) { + // remove 'leaf' class containing
  3. element + var parent = _this.parents('li.leaf'); + parent.remove(); + }); +} + function toggleSubmodules(e) { e.preventDefault(); $(this).toggleClass('expand').toggleClass('collapse'); diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index 12b0d33039..2605277e71 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -6,13 +6,13 @@ This def will enumerate through a passed in subsection and list all of the units <%def name="enum_units(subsection)">
      % for unit in subsection.get_children(): -
    1. +
    2. @@ -25,3 +25,6 @@ This def will enumerate through a passed in subsection and list all of the units
    + + + diff --git a/cms/urls.py b/cms/urls.py index 7cbb912b06..b57f894acb 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -11,7 +11,6 @@ urlpatterns = ('', url(r'^$', 'contentstore.views.index', name='index'), url(r'^edit/(?P.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^subsection/(?P.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), - url(r'^delete/(?P.*?)$', 'contentstore.views.delete_unit', name='delete_unit'), url(r'^preview_component/(?P.*?)$', 'contentstore.views.preview_component', name='preview_component'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'), From d44d9ff2d1a5a380f00711b0e7e852755bb2aed0 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:07:47 -0400 Subject: [PATCH 0063/1010] tweaked signup template --- cms/templates/signup.html | 101 ++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/cms/templates/signup.html b/cms/templates/signup.html index f22e3c7950..1afd89ee17 100644 --- a/cms/templates/signup.html +++ b/cms/templates/signup.html @@ -1,52 +1,59 @@ <%inherit file="base.html" /> <%block name="title">Sign up +<%block name="bodyclass">no-header <%block name="content"> -
    - -
    -
    -

    Sign Up for edX

    -
    -
    - -
    - -
    -
    - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - + - -
    - -
    - + \ No newline at end of file From f4ccb335e56a0a43dfeab56aeeb9d2bb32636a27 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:08:20 -0400 Subject: [PATCH 0064/1010] abstracted classes to work for both log in and sign up forms --- cms/static/sass/_login.scss | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cms/static/sass/_login.scss b/cms/static/sass/_login.scss index 9d228e7bab..12a276fadd 100644 --- a/cms/static/sass/_login.scss +++ b/cms/static/sass/_login.scss @@ -1,6 +1,7 @@ +.sign-up-box, .log-in-box { width: 500px; - margin: 200px auto; + margin: 100px auto; border-radius: 3px; header { @@ -19,7 +20,7 @@ } } - .log-in-form { + form { padding: 40px; border: 1px solid $darkGrey; border-top-width: 0; @@ -34,22 +35,33 @@ font-weight: 700; } - .email-field, - .password-field { + input[type="text"], + input[type="email"], + input[type="password"] { width: 100%; font-size: 20px; font-weight: 300; } .row { + @include clearfix; margin-bottom: 24px; + + .split { + float: left; + width: 48%; + + &:first-child { + margin-right: 4%; + } + } } .form-actions { margin-bottom: 0; } - .log-in-button { + input[type="submit"] { @include blue-button; margin-right: 10px; padding: 8px 20px 10px; From 222d23dfd3fccec39b6f5563409e2a20ae712bc4 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:10:00 -0400 Subject: [PATCH 0065/1010] added class to remove header --- cms/static/sass/_header.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index 0992d49210..0fa4f2eddd 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -1,3 +1,9 @@ +body.no-header { + .primary-header { + display: none; + } +} + .primary-header { width: 100%; height: 36px; From 277cc3433c8842cb730680ff641ebc1dd27181e5 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:10:49 -0400 Subject: [PATCH 0066/1010] added no-header class to login.html --- cms/templates/login.html | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/templates/login.html b/cms/templates/login.html index be6968b77a..9b48f525b4 100644 --- a/cms/templates/login.html +++ b/cms/templates/login.html @@ -1,6 +1,7 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> <%block name="title">Log in +<%block name="bodyclass">no-header <%block name="content"> From 5a26dd7ebaceeea302c254c2610d53b7e177d84d Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:55:23 -0400 Subject: [PATCH 0067/1010] removed extraneous border on log in extras --- cms/static/sass/_login.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/static/sass/_login.scss b/cms/static/sass/_login.scss index 12a276fadd..48216636d0 100644 --- a/cms/static/sass/_login.scss +++ b/cms/static/sass/_login.scss @@ -75,6 +75,5 @@ margin-top: 10px; text-align: right; font-size: 13px; - border-top: 1px solid $lightGrey; } } \ No newline at end of file From ea1b7caf47b0fa6eeed3fdc3156a93c67a6e7b8c Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 3 Oct 2012 12:56:50 -0400 Subject: [PATCH 0068/1010] consistent field labels --- cms/templates/login.html | 4 ++-- cms/templates/signup.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/templates/login.html b/cms/templates/login.html index 9b48f525b4..7d225678dc 100644 --- a/cms/templates/login.html +++ b/cms/templates/login.html @@ -11,11 +11,11 @@
  4. element --- cms/djangoapps/contentstore/views.py | 39 ++++++++++++++++++- cms/static/js/base.js | 30 +++++++++++--- cms/templates/widgets/units.html | 4 +- cms/urls.py | 1 + common/lib/xmodule/xmodule/vertical_module.py | 4 ++ 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b6aeebd602..53923e5c6b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -45,6 +45,8 @@ from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_ from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME from .utils import get_course_location_for_item +from xmodule.templates import all_templates + log = logging.getLogger(__name__) @@ -126,9 +128,14 @@ def course_index(request, org, course, name): course = modulestore().get_item(location) sections = course.get_children() + # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the + # code. We should probably refactor + template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) + return render_to_response('overview.html', { 'sections': sections, - 'upload_asset_callback_url': upload_asset_callback_url + 'upload_asset_callback_url': upload_asset_callback_url, + 'create_new_unit_template': template.location }) @@ -144,8 +151,14 @@ def edit_subsection(request, location): if item.location.category != 'sequential': return HttpResponseBadRequest + # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the + # code. We should probably refactor + template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) + return render_to_response('edit_subsection.html', - {'subsection':item}) + {'subsection':item, + 'create_new_unit_template' : template.location + }) @login_required def edit_unit(request, location): @@ -407,6 +420,20 @@ def delete_item(request): return HttpResponse() +@login_required +@expect_json +def create_item(request): + # parent_location should be the location of the parent container + parent_location = request.POST['parent_id'] + + # which type of item to create + category = request.POST['category'] + + # check permissions for this user within this course + if not has_access(request.user, parent_location): + raise PermissionDenied() + + @login_required @expect_json @@ -446,6 +473,10 @@ def save_item(request): def clone_item(request): parent_location = Location(request.POST['parent_location']) template = Location(request.POST['template']) + + display_name = None + if 'display_name' in request.POST: + display_name = request.POST['display_name'] if not has_access(request.user, parent_location): raise PermissionDenied() @@ -457,6 +488,10 @@ def clone_item(request): # TODO: This needs to be deleted when we have proper storage for static content new_item.metadata['data_dir'] = parent.metadata['data_dir'] + + # replace the display name with an optional parameter passed in from the caller + if display_name is not None: + new_item.metadata['display_name'] = display_name modulestore().update_metadata(new_item.location.url(), new_item.own_metadata) modulestore().update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 8d1af795b3..364cb61f3d 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -24,19 +24,39 @@ $(document).ready(function() { $('.assets .upload-button').bind('click', showUploadModal); $('.upload-modal .close-button').bind('click', hideModal); $('.unit .item-actions .delete-button').bind('click', deleteUnit); + $('.new-unit-item').bind('click', createNewUnit); }); +function createNewUnit(e) { + e.preventDefault(); + + parent = $(this).data('parent'); + template = $(this).data('template'); + + $.post('/clone_item', + {'parent_location' : parent, + 'template' : template, + 'display_name': 'New Unit', + }, + function(data) { + // redirect to the edit page + window.location = "/edit/" + data['id']; + }); +} + function deleteUnit(e) { e.preventDefault(); - var id = $(this).data('id'); - var _this = $(this); + + if(!confirm('Are you sure you wish to delete this item. It cannot be reversed!')) + return; + + var _li_el = $(this).parents('li.leaf'); + var id = _li_el.data('id'); $.post('/delete_item', {'id': id, 'delete_children' : 'true'}, function(data) { - // remove 'leaf' class containing
  5. element - var parent = _this.parents('li.leaf'); - parent.remove(); + _li_el.remove(); }); } diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index 2605277e71..9d9c7943da 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -6,7 +6,7 @@ This def will enumerate through a passed in subsection and list all of the units <%def name="enum_units(subsection)">
      % for unit in subsection.get_children(): -
    1. +
    2. % endfor
    3. - + New Unit
    4. diff --git a/cms/urls.py b/cms/urls.py index ae22220511..f57f8d91d2 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -13,6 +13,7 @@ urlpatterns = ('', url(r'^subsection/(?P.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^preview_component/(?P.*?)$', 'contentstore.views.preview_component', name='preview_component'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), + url(r'^create_item$', 'contentstore.views.create_item', name='create_item'), url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'), url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py index f0c26e045f..738ee9ac5f 100644 --- a/common/lib/xmodule/xmodule/vertical_module.py +++ b/common/lib/xmodule/xmodule/vertical_module.py @@ -45,5 +45,9 @@ class VerticalModule(XModule): class VerticalDescriptor(SequenceDescriptor): module_class = VerticalModule + # cdodge: override the SequenceDescript's template_dir_name to point to default template directory + template_dir_name = "default" + js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]} js_module_name = "VerticalDescriptor" + From 29c142e3dcf974373e8fb943b49b284cd5e996d0 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 11:09:28 -0400 Subject: [PATCH 0075/1010] need to wire through the new unit template location to the edit unit page --- cms/djangoapps/contentstore/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 1ac82bc213..e044227f43 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -210,6 +210,10 @@ def edit_unit(request, location): containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) + # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the + # code. We should probably refactor + template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) + return render_to_response('unit.html', { 'unit': item, 'components': components, @@ -217,6 +221,7 @@ def edit_unit(request, location): 'lms_link': lms_link, 'subsection': containing_subsection, 'section': containing_section, + 'create_new_unit_template' : template.location }) From d93bf63dff756ac67721b47e02cb5a02d8d7e416 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 13:05:45 -0400 Subject: [PATCH 0076/1010] save display_name and subtitle metadata --- cms/static/js/base.js | 32 ++++++++++++++++++++++++++++++ cms/templates/edit_subsection.html | 6 +++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 364cb61f3d..59466d3b96 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -25,8 +25,40 @@ $(document).ready(function() { $('.upload-modal .close-button').bind('click', hideModal); $('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.new-unit-item').bind('click', createNewUnit); + $('.save-subsection').bind('click', saveSubsection); }); +function saveSubsection(e) { + e.preventDefault(); + + var id = $(this).data('id'); + + // pull all metadata editable fields on page + var metadata_fields = $('input[data-metadata-name]'); + + metadata = {}; + for(var i=0; i< metadata_fields.length;i++) { + el = metadata_fields[i]; + metadata[$(el).data("metadata-name")] = el.value; + } + + children =[]; + + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : id, 'metadata' : metadata, 'data': null, 'children' : children}), + success: function() { + alert('Your changes have been saved.'); + }, + error: function() { + alert('There has been an error while saving your changes.'); + } + }); +} + function createNewUnit(e) { e.preventDefault(); diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index c9fe9fbda8..578e2a9ceb 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -12,11 +12,11 @@
      - +
      - +
      @@ -55,7 +55,7 @@ hideshow
      From 48d84c2e3d2d2d7aadc865e2d58227edb0873096 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 13:44:46 -0400 Subject: [PATCH 0077/1010] support reordering of units inside subsection list --- cms/static/js/base.js | 27 +++++++++++++++++++++++++++ cms/templates/widgets/units.html | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 59466d3b96..48789bbae8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -26,8 +26,35 @@ $(document).ready(function() { $('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.new-unit-item').bind('click', createNewUnit); $('.save-subsection').bind('click', saveSubsection); + + // making the unit list sortable + $('.sortable-unit-list').sortable(); + $('.sortable-unit-list').disableSelection(); + $('.sortable-unit-list').bind('sortstop', onUnitReordered); }); +// This method only changes the ordering of the child objects in a subsection +function onUnitReordered() { + var subsection_id = $(this).data('subsection-id'); + + var _els = $(this).children('li:.leaf'); + + var children = new Array(); + for(var i=0;i<_els.length;i++) { + el = _els[i]; + children[i] = $(el).data('id'); + } + + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : subsection_id, 'metadata' : null, 'data': null, 'children' : children}) + }); +} + function saveSubsection(e) { e.preventDefault(); diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index cb9d06db4c..ee7c02dedb 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -3,8 +3,8 @@ -<%def name="enum_units(subsection, actions=True, selected=None)"> -
        +<%def name="enum_units(subsection, actions=True, selected=None, sortable=True)"> +
          % for unit in subsection.get_children():
        1. <% From fe42f6fd22bd2570b58661157a99356eeacc1451 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 14:00:25 -0400 Subject: [PATCH 0078/1010] address pull request feedback --- cms/djangoapps/contentstore/views.py | 44 +++++----------------------- cms/static/js/base.js | 2 +- cms/templates/widgets/units.html | 4 +-- cms/urls.py | 1 - 4 files changed, 10 insertions(+), 41 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index e044227f43..b8da38f2c7 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -128,14 +128,10 @@ def course_index(request, org, course, name): course = modulestore().get_item(location) sections = course.get_children() - # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the - # code. We should probably refactor - template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) - return render_to_response('overview.html', { 'sections': sections, 'upload_asset_callback_url': upload_asset_callback_url, - 'create_new_unit_template': template.location + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') }) @@ -151,13 +147,9 @@ def edit_subsection(request, location): if item.location.category != 'sequential': return HttpResponseBadRequest - # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the - # code. We should probably refactor - template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) - return render_to_response('edit_subsection.html', {'subsection': item, - 'create_new_unit_template' : template.location + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') }) @login_required @@ -210,10 +202,6 @@ def edit_unit(request, location): containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) - # This knowledge of what the 'new template' should be seems like it needs to be kept deeper down in the - # code. We should probably refactor - template = modulestore().get_item(Location('i4x', 'edx', 'templates', 'vertical', 'Empty')) - return render_to_response('unit.html', { 'unit': item, 'components': components, @@ -221,7 +209,7 @@ def edit_unit(request, location): 'lms_link': lms_link, 'subsection': containing_subsection, 'section': containing_section, - 'create_new_unit_template' : template.location + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') }) @@ -412,7 +400,7 @@ def _delete_item(item, recurse=False): if recurse: children = item.get_children() for child in children: - _delete_item(child) + _delete_item(child, recurse) modulestore().delete_item(item.location); @@ -427,30 +415,14 @@ def delete_item(request): raise PermissionDenied() # optional parameter to delete all children (default False) - delete_children = False - if 'delete_children' in request.POST: - delete_children = request.POST['delete_children'] in ['true', 'True'] + delete_children = request.POST.get('delete_children', False) item = modulestore().get_item(item_location) - _delete_item(item) + _delete_item(item, delete_children) return HttpResponse() -@login_required -@expect_json -def create_item(request): - # parent_location should be the location of the parent container - parent_location = request.POST['parent_id'] - - # which type of item to create - category = request.POST['category'] - - # check permissions for this user within this course - if not has_access(request.user, parent_location): - raise PermissionDenied() - - @login_required @expect_json @@ -491,9 +463,7 @@ def clone_item(request): parent_location = Location(request.POST['parent_location']) template = Location(request.POST['template']) - display_name = None - if 'display_name' in request.POST: - display_name = request.POST['display_name'] + display_name = request.POST.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 48789bbae8..66f76c265f 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -113,7 +113,7 @@ function deleteUnit(e) { var id = _li_el.data('id'); $.post('/delete_item', - {'id': id, 'delete_children' : 'true'}, + {'id': id, 'delete_children' : true}, function(data) { _li_el.remove(); }); diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index ee7c02dedb..8207b485a7 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -22,14 +22,14 @@ This def will enumerate through a passed in subsection and list all of the units % if actions:
          - +
          % endif
        2. % endfor
        3. - + New Unit
        4. diff --git a/cms/urls.py b/cms/urls.py index 0caecd654a..44f42343f3 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -13,7 +13,6 @@ urlpatterns = ('', url(r'^subsection/(?P.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^preview_component/(?P.*?)$', 'contentstore.views.preview_component', name='preview_component'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), - url(r'^create_item$', 'contentstore.views.create_item', name='create_item'), url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'), url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', From c3aa86f1fb6ea69107eff750afd58650041a19e7 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 14:53:43 -0400 Subject: [PATCH 0079/1010] remove template_dir_name from Vertical and Sequence descriptors so that it uses the default template dir --- common/lib/xmodule/xmodule/seq_module.py | 2 -- common/lib/xmodule/xmodule/vertical_module.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index bcd2932537..0ade3e0e7d 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -118,8 +118,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): stores_state = True # For remembering where in the sequence the student is - template_dir_name = 'sequence' - js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]} js_module_name = "SequenceDescriptor" diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py index 738ee9ac5f..397bd3e136 100644 --- a/common/lib/xmodule/xmodule/vertical_module.py +++ b/common/lib/xmodule/xmodule/vertical_module.py @@ -45,9 +45,6 @@ class VerticalModule(XModule): class VerticalDescriptor(SequenceDescriptor): module_class = VerticalModule - # cdodge: override the SequenceDescript's template_dir_name to point to default template directory - template_dir_name = "default" - js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]} js_module_name = "VerticalDescriptor" From ebd3f28de83f8448f3b21a1dd9d8bb618c794391 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 4 Oct 2012 09:52:18 -0400 Subject: [PATCH 0080/1010] added padding to xmodule units --- cms/static/sass/_unit.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 0cc855a56f..38981d2563 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -220,6 +220,10 @@ } } + .xmodule_display { + padding: 10px 20px; + } + .component-editor { @include edit-box; display: none; From 61ea9d367822f6091c776f70b9a3c2e63c7a89c0 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 4 Oct 2012 10:00:45 -0400 Subject: [PATCH 0081/1010] fixed log in header and overview search --- cms/static/sass/_login.scss | 1 + cms/templates/overview.html | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/_login.scss b/cms/static/sass/_login.scss index 48216636d0..960a44272b 100644 --- a/cms/static/sass/_login.scss +++ b/cms/static/sass/_login.scss @@ -13,6 +13,7 @@ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset; h1 { + float: none; margin: 5px 0; font-size: 15px; font-weight: 300; diff --git a/cms/templates/overview.html b/cms/templates/overview.html index f4ae4abe4d..d650f5e6df 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -9,7 +9,9 @@

          Courseware

          - +
          + +
          New Section % for section in sections: From fbae5d9e7173db3dc7995803f66a1b5560114132 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 4 Oct 2012 11:16:10 -0400 Subject: [PATCH 0082/1010] fixed courseware overview search positioning --- cms/static/sass/_courseware.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss index 020548e08a..a888b65ed5 100644 --- a/cms/static/sass/_courseware.scss +++ b/cms/static/sass/_courseware.scss @@ -1,7 +1,5 @@ input.courseware-unit-search-input { - position: absolute; - right: 0; - top: 5px; + float: left; width: 260px; background-color: #fff; } From 842306addd36aaa1f3e05bd4d0e0da0642ae8dfd Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Fri, 5 Oct 2012 09:18:10 -0400 Subject: [PATCH 0083/1010] updated styles and template for unit location tree --- cms/static/sass/_cms_mixins.scss | 2 +- cms/static/sass/_unit.scss | 2 +- cms/templates/unit.html | 47 +++++++++----------------------- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index 8ca737f33d..e4ecc953e9 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -153,7 +153,7 @@ } &.editing { - background: #d1dae3; + background: #fffcf1; } .draft-item, diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 38981d2563..8c9e54cc0f 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -386,7 +386,7 @@ } .new-unit-item { - margin-left: 50px; + margin: 0 0 15px 50px; } } } diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 16d337f57b..3d48461231 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -92,46 +92,25 @@

          Unit Location

          -
          -
          -
          -

          ${section.display_name}

          -
          -
          -
          +
            +
          1. + ${section.display_name}
              -
            1. - - ${units.enum_units(subsection, actions=False, selected=unit.location)} +
            2. + + + ${subsection.display_name} + +
                + ${units.enum_units(subsection, actions=False, selected=unit.location)} +
            -
          -
          + +
        - -
        - - -
        - - From 1e68123e09d7f53302b973865ab30369fe668bde Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Fri, 5 Oct 2012 09:21:20 -0400 Subject: [PATCH 0084/1010] fixed sign up to log in text --- cms/templates/signup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/signup.html b/cms/templates/signup.html index 67c70b5cac..77a5df7856 100644 --- a/cms/templates/signup.html +++ b/cms/templates/signup.html @@ -51,7 +51,7 @@
      From 758c4469291634893b50ab076027441a0546da46 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 09:56:07 -0400 Subject: [PATCH 0085/1010] wip on setting date/times on subsection page --- cms/djangoapps/contentstore/utils.py | 28 +- cms/djangoapps/contentstore/views.py | 19 +- cms/static/js/base.js | 23 +- cms/templates/edit_subsection.html | 41 +- .../static/js/vendor/timepicker/datepair.js | 197 ++++++ .../vendor/timepicker/jquery.timepicker.css | 51 ++ .../js/vendor/timepicker/jquery.timepicker.js | 585 ++++++++++++++++++ .../timepicker/jquery.timepicker.min.js | 1 + 8 files changed, 914 insertions(+), 31 deletions(-) create mode 100644 common/static/js/vendor/timepicker/datepair.js create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.css create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.js create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.min.js diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index fc801ac684..4000f011ba 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,13 +1,14 @@ +from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -''' -cdodge: for a given Xmodule, return the course that it belongs to -NOTE: This makes a lot of assumptions about the format of the course location -Also we have to assert that this module maps to only one course item - it'll throw an -assert if not -''' def get_course_location_for_item(location): + ''' + cdodge: for a given Xmodule, return the course that it belongs to + NOTE: This makes a lot of assumptions about the format of the course location + Also we have to assert that this module maps to only one course item - it'll throw an + assert if not + ''' item_loc = Location(location) # check to see if item is already a course, if so we can skip this @@ -29,3 +30,18 @@ def get_course_location_for_item(location): location = courses[0].location return location + + +def get_lms_link_for_item(item): + if settings.LMS_BASE is not None: + lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( + lms_base=settings.LMS_BASE, + # TODO: These will need to be changed to point to the particular instance of this problem in the particular course + course_id = modulestore().get_containing_courses(item.location)[0].id, + location=item.location, + ) + else: + lms_link = None + + return lms_link + diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b8da38f2c7..fe9204c21a 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME -from .utils import get_course_location_for_item +from .utils import get_course_location_for_item, get_lms_link_for_item from xmodule.templates import all_templates @@ -143,13 +143,18 @@ def edit_subsection(request, location): item = modulestore().get_item(location) + lms_link = get_lms_link_for_item(item) + # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': return HttpResponseBadRequest + logging.debug('Start = {0}'.format(item.start)) + return render_to_response('edit_subsection.html', {'subsection': item, - 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), + 'lms_link': lms_link }) @login_required @@ -167,15 +172,7 @@ def edit_unit(request, location): item = modulestore().get_item(location) - if settings.LMS_BASE is not None: - lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( - lms_base=settings.LMS_BASE, - # TODO: These will need to be changed to point to the particular instance of this problem in the particular course - course_id = modulestore().get_containing_courses(item.location)[0].id, - location=item.location, - ) - else: - lms_link = None + lms_link = get_lms_link_for_item(item) component_templates = defaultdict(list) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 66f76c265f..3508c80aed 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -31,6 +31,11 @@ $(document).ready(function() { $('.sortable-unit-list').sortable(); $('.sortable-unit-list').disableSelection(); $('.sortable-unit-list').bind('sortstop', onUnitReordered); + + // expand/collapse methods for optional date setters + $('.set-date').bind('click', showDateSetter); + $('.remove-date').bind('click', removeDateSetter); + }); // This method only changes the ordering of the child objects in a subsection @@ -198,8 +203,22 @@ function hideHistoryModal(e) { $modalCover.hide(); } - - +function showDateSetter(e) { + e.preventDefault(); + var $block = $(this).closest('.due-date-input'); + $(this).hide(); + $block.find('.date-setter').show(); +} + +function removeDateSetter(e) { + e.preventDefault(); + var $block = $(this).closest('.due-date-input'); + $block.find('.date-setter').hide(); + $block.find('.set-date').show(); + // clear out the values + $block.find('.date').val(''); + $block.find('.time').val(''); +} diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 578e2a9ceb..b05c2121fc 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,9 +1,19 @@ <%inherit file="base.html" /> +<%! + import time + import dateutil.parser + from datetime import datetime + + now = datetime.now() +%> + <%! from django.core.urlresolvers import reverse %> <%block name="bodyclass">subsection <%block name="title">CMS Subsection <%namespace name="units" file="widgets/units.html" /> +<%namespace name='static' file='static_content.html'/> +<%namespace name='datetime' module='datetime'/> <%block name="content">
      @@ -15,8 +25,8 @@
      - - + +
      @@ -35,31 +45,38 @@
      -
      - - +
      + +

      The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.

      Set a due date -
      -

      +

      +

      + <% + due_date = dateutil.parser.parse(subsection.metadata.get('get')) if 'due' in subsection.metadata else None + %> + + Remove due date

      -
      - - hideshow -
      + +<%block name="jsextra"> + + + + diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js new file mode 100644 index 0000000000..d547925e5b --- /dev/null +++ b/common/static/js/vendor/timepicker/datepair.js @@ -0,0 +1,197 @@ +$(function() { + + $('.datepair input.date').each(function(){ + var $this = $(this); + $this.datepicker({ 'dateFormat': 'm/d/yy' }); + + if ($this.hasClass('start') || $this.hasClass('end')) { + $this.on('changeDate change', doDatepair); + } + + }); + + $('.datepair input.time').each(function() { + var $this = $(this); + var opts = { 'showDuration': true, 'timeFormat': 'g:ia', 'scrollDefaultNow': true }; + + if ($this.hasClass('start') || $this.hasClass('end')) { + opts.onSelect = doDatepair; + } + + $this.timepicker(opts); + }); + + $('.datepair').each(initDatepair); + + function initDatepair() + { + var container = $(this); + + var startDateInput = container.find('input.start.date'); + var endDateInput = container.find('input.end.date'); + var dateDelta = 0; + + if (startDateInput.length && endDateInput.length) { + var startDate = new Date(startDateInput.val()); + var endDate = new Date(endDateInput.val()); + + dateDelta = endDate.getTime() - startDate.getTime(); + + container.data('dateDelta', dateDelta); + } + + var startTimeInput = container.find('input.start.time'); + var endTimeInput = container.find('input.end.time'); + + if (startTimeInput.length && endTimeInput.length) { + var startInt = startTimeInput.timepicker('getSecondsFromMidnight'); + var endInt = endTimeInput.timepicker('getSecondsFromMidnight'); + + container.data('timeDelta', endInt - startInt); + + if (dateDelta < 86400000) { + endTimeInput.timepicker('option', 'minTime', startInt); + } + } + } + + function doDatepair() + { + var target = $(this); + if (target.val() == '') { + return; + } + + var container = target.closest('.datepair'); + + if (target.hasClass('date')) { + updateDatePair(target, container); + + } else if (target.hasClass('time')) { + updateTimePair(target, container); + } + } + + function updateDatePair(target, container) + { + var start = container.find('input.start.date'); + var end = container.find('input.end.date'); + + if (!start.length || !end.length) { + return; + } + + var startDate = new Date(start.val()); + var endDate = new Date(end.val()); + + var oldDelta = container.data('dateDelta'); + + if (oldDelta && target.hasClass('start')) { + var newEnd = new Date(startDate.getTime()+oldDelta); + end.val(newEnd.format('m/d/Y')); + end.datepicker('update'); + return; + + } else { + var newDelta = endDate.getTime() - startDate.getTime(); + + if (newDelta < 0) { + newDelta = 0; + + if (target.hasClass('start')) { + end.val(startDate.format('m/d/Y')); + end.datepicker('update'); + } else if (target.hasClass('end')) { + start.val(endDate.format('m/d/Y')); + start.datepicker('update'); + } + } + + if (newDelta < 86400000) { + var startTimeVal = container.find('input.start.time').val(); + + if (startTimeVal) { + container.find('input.end.time').timepicker('option', {'minTime': startTimeVal}); + } + } else { + container.find('input.end.time').timepicker('option', {'minTime': null}); + } + + container.data('dateDelta', newDelta); + } + } + + function updateTimePair(target, container) + { + var start = container.find('input.start.time'); + var end = container.find('input.end.time'); + + if (!start.length || !end.length) { + return; + } + + var startInt = start.timepicker('getSecondsFromMidnight'); + var endInt = end.timepicker('getSecondsFromMidnight'); + + var oldDelta = container.data('timeDelta'); + var dateDelta = container.data('dateDelta'); + + if (target.hasClass('start') && (!dateDelta || dateDelta < 86400000)) { + end.timepicker('option', 'minTime', startInt); + } + + var endDateAdvance = 0; + var newDelta; + + if (oldDelta && target.hasClass('start')) { + // lock the duration and advance the end time + + var newEnd = (startInt+oldDelta)%86400; + + if (newEnd < 0) { + newEnd += 86400; + } + + end.timepicker('setTime', newEnd); + newDelta = newEnd - startInt; + } else if (startInt !== null && endInt !== null) { + newDelta = endInt - startInt; + } else { + return; + } + + container.data('timeDelta', newDelta); + + if (newDelta < 0 && (!oldDelta || oldDelta > 0)) { + // overnight time span. advance the end date 1 day + var endDateAdvance = 86400000; + + } else if (newDelta > 0 && oldDelta < 0) { + // switching from overnight to same-day time span. decrease the end date 1 day + var endDateAdvance = -86400000; + } + + var startInput = container.find('.start.date'); + var endInput = container.find('.end.date'); + + if (startInput.val() && !endInput.val()) { + endInput.val(startInput.val()); + endInput.datepicker('update'); + dateDelta = 0; + container.data('dateDelta', 0); + } + + if (endDateAdvance != 0) { + if (dateDelta || dateDelta === 0) { + var endDate = new Date(endInput.val()); + var newEnd = new Date(endDate.getTime() + endDateAdvance); + endInput.val(newEnd.format('m/d/Y')); + endInput.datepicker('update'); + container.data('dateDelta', dateDelta + endDateAdvance); + } + } + } +}); + +// Simulates PHP's date function +Date.prototype.format=function(format){var returnStr='';var replace=Date.replaceChars;for(var i=0;i'); + var attrs = { 'type': 'text', 'value': self.val() }; + var raw_attrs = self[0].attributes; + + for (var i=0; i < raw_attrs.length; i++) { + attrs[raw_attrs[i].nodeName] = raw_attrs[i].nodeValue; + } + + input.attr(attrs); + self.replaceWith(input); + self = input; + } + + var settings = $.extend({}, _defaults); + + if (options) { + settings = $.extend(settings, options); + } + + if (settings.minTime) { + settings.minTime = _time2int(settings.minTime); + } + + if (settings.maxTime) { + settings.maxTime = _time2int(settings.maxTime); + } + + if (settings.durationTime) { + settings.durationTime = _time2int(settings.durationTime); + } + + if (settings.lang) { + _lang = $.extend(_lang, settings.lang); + } + + self.data('timepicker-settings', settings); + self.attr('autocomplete', 'off'); + self.click(methods.show).focus(methods.show).blur(_formatValue).keydown(_keyhandler); + self.addClass('ui-timepicker-input'); + + if (self.val()) { + var prettyTime = _int2time(_time2int(self.val()), settings.timeFormat); + self.val(prettyTime); + } + + // close the dropdown when container loses focus + $("body").attr("tabindex", -1).focusin(function(e) { + if ($(e.target).closest('.ui-timepicker-input').length == 0 && $(e.target).closest('.ui-timepicker-list').length == 0) { + methods.hide(); + } + }); + + }); + }, + + show: function(e) + { + var self = $(this); + var list = self.data('timepicker-list'); + + // check if list needs to be rendered + if (!list || list.length == 0) { + _render(self); + list = self.data('timepicker-list'); + } + + // check if a flag was set to close this picker + if (self.hasClass('ui-timepicker-hideme')) { + self.removeClass('ui-timepicker-hideme'); + list.hide(); + return; + } + + if (list.is(':visible')) { + return; + } + + // make sure other pickers are hidden + methods.hide(); + + + var topMargin = parseInt(self.css('marginTop').slice(0, -2)); + if (!topMargin) topMargin = 0; // correct for IE returning "auto" + + if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { + // position the dropdown on top + list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin - list.outerHeight() }); + } else { + // put it under the input + list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin + self.outerHeight() }); + } + + list.show(); + + var settings = self.data('timepicker-settings'); + // position scrolling + var selected = list.find('.ui-timepicker-selected'); + + if (!selected.length) { + if (self.val()) { + selected = _findRow(self, list, _time2int(self.val())); + } else if (settings.minTime === null && settings.scrollDefaultNow) { + selected = _findRow(self, list, _time2int(new Date())); + } else if (settings.scrollDefaultTime !== false) { + selected = _findRow(self, list, _time2int(settings.scrollDefaultTime)); + } + } + + if (selected && selected.length) { + var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight(); + list.scrollTop(topOffset); + } else { + list.scrollTop(0); + } + + self.trigger('showTimepicker'); + }, + + hide: function(e) + { + $('.ui-timepicker-list:visible').each(function() { + var list = $(this); + var self = list.data('timepicker-input'); + var settings = self.data('timepicker-settings'); + if (settings.selectOnBlur) { + _selectValue(self); + } + + list.hide(); + self.trigger('hideTimepicker'); + }); + }, + + option: function(key, value) + { + var self = $(this); + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (typeof key == 'object') { + settings = $.extend(settings, key); + + } else if (typeof key == 'string' && typeof value != 'undefined') { + settings[key] = value; + + } else if (typeof key == 'string') { + return settings[key]; + } + + if (settings.minTime) { + settings.minTime = _time2int(settings.minTime); + } + + if (settings.maxTime) { + settings.maxTime = _time2int(settings.maxTime); + } + + if (settings.durationTime) { + settings.durationTime = _time2int(settings.durationTime); + } + + self.data('timepicker-settings', settings); + + if (list) { + list.remove(); + self.data('timepicker-list', false); + } + + }, + + getSecondsFromMidnight: function() + { + return _time2int($(this).val()); + }, + + getTime: function() + { + return new Date(_baseDate.valueOf() + (_time2int($(this).val())*1000)); + }, + + setTime: function(value) + { + var self = $(this); + var prettyTime = _int2time(_time2int(value), self.data('timepicker-settings').timeFormat); + self.val(prettyTime); + } + + }; + + // private methods + + function _render(self) + { + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (list && list.length) { + list.remove(); + self.data('timepicker-list', false); + } + + list = $('
        '); + list.attr('tabindex', -1); + list.addClass('ui-timepicker-list'); + if (settings.className) { + list.addClass(settings.className); + } + + list.css({'display':'none', 'position': 'absolute' }); + + if (settings.minTime !== null && settings.showDuration) { + list.addClass('ui-timepicker-with-duration'); + } + + var durStart = (settings.durationTime !== null) ? settings.durationTime : settings.minTime; + var start = (settings.minTime !== null) ? settings.minTime : 0; + var end = (settings.maxTime !== null) ? settings.maxTime : (start + _ONE_DAY - 1); + + if (end <= start) { + // make sure the end time is greater than start time, otherwise there will be no list to show + end += _ONE_DAY; + } + + for (var i=start; i <= end; i += settings.step*60) { + var timeInt = i%_ONE_DAY; + var row = $('
      • '); + row.data('time', timeInt) + row.text(_int2time(timeInt, settings.timeFormat)); + + if (settings.minTime !== null && settings.showDuration) { + var duration = $(''); + duration.addClass('ui-timepicker-duration'); + duration.text(' ('+_int2duration(i - durStart)+')'); + row.append(duration) + } + + list.append(row); + } + + list.data('timepicker-input', self); + self.data('timepicker-list', list); + + $('body').append(list); + _setSelected(self, list); + + list.delegate('li', 'click', { 'timepicker': self }, function(e) { + self.addClass('ui-timepicker-hideme'); + self[0].focus(); + + // make sure only the clicked row is selected + list.find('li').removeClass('ui-timepicker-selected'); + $(this).addClass('ui-timepicker-selected'); + + _selectValue(self); + list.hide(); + }); + }; + + function _findRow(self, list, value) + { + if (!value && value !== 0) { + return false; + } + + var settings = self.data('timepicker-settings'); + var out = false; + + // loop through the menu items + list.find('li').each(function(i, obj) { + var jObj = $(obj); + + // check if the value is less than half a step from each row + if (Math.abs(jObj.data('time') - value) <= settings.step*30) { + out = jObj; + return false; + } + }); + + return out; + } + + function _setSelected(self, list) + { + var timeValue = _time2int(self.val()); + + var selected = _findRow(self, list, timeValue); + if (selected) selected.addClass('ui-timepicker-selected'); + } + + + function _formatValue() + { + if (this.value == '') { + return; + } + + var self = $(this); + var prettyTime = _int2time(_time2int(this.value), self.data('timepicker-settings').timeFormat); + self.val(prettyTime); + } + + function _keyhandler(e) + { + var self = $(this); + var list = self.data('timepicker-list'); + + if (!list.is(':visible')) { + if (e.keyCode == 40) { + self.focus(); + } else { + return true; + } + }; + + switch (e.keyCode) { + + case 13: // return + _selectValue(self); + methods.hide.apply(this); + e.preventDefault(); + return false; + break; + + case 38: // up + var selected = list.find('.ui-timepicker-selected'); + + if (!selected.length) { + var selected; + list.children().each(function(i, obj) { + if ($(obj).position().top > 0) { + selected = $(obj); + return false; + } + }); + selected.addClass('ui-timepicker-selected'); + + } else if (!selected.is(':first-child')) { + selected.removeClass('ui-timepicker-selected'); + selected.prev().addClass('ui-timepicker-selected'); + + if (selected.prev().position().top < selected.outerHeight()) { + list.scrollTop(list.scrollTop() - selected.outerHeight()); + } + } + + break; + + case 40: // down + var selected = list.find('.ui-timepicker-selected'); + + if (selected.length == 0) { + var selected; + list.children().each(function(i, obj) { + if ($(obj).position().top > 0) { + selected = $(obj); + return false; + } + }); + + selected.addClass('ui-timepicker-selected'); + } else if (!selected.is(':last-child')) { + selected.removeClass('ui-timepicker-selected'); + selected.next().addClass('ui-timepicker-selected'); + + if (selected.next().position().top + 2*selected.outerHeight() > list.outerHeight()) { + list.scrollTop(list.scrollTop() + selected.outerHeight()); + } + } + + break; + + case 27: // escape + list.find('li').removeClass('ui-timepicker-selected'); + list.hide(); + break; + + case 9: + case 16: + case 17: + case 18: + case 19: + case 20: + case 33: + case 34: + case 35: + case 36: + case 37: + case 39: + case 45: + return; + + default: + list.find('li').removeClass('ui-timepicker-selected'); + return; + } + }; + + function _selectValue(self) + { + var settings = self.data('timepicker-settings') + var list = self.data('timepicker-list'); + var timeValue = null; + + var cursor = list.find('.ui-timepicker-selected'); + + if (cursor.length) { + // selected value found + var timeValue = cursor.data('time'); + + } else if (self.val()) { + + // no selected value; fall back on input value + var timeValue = _time2int(self.val()); + + _setSelected(self, list); + } + + if (timeValue !== null) { + var timeString = _int2time(timeValue, settings.timeFormat); + self.attr('value', timeString); + } + + self.trigger('change').trigger('changeTime'); + }; + + function _int2duration(seconds) + { + var minutes = Math.round(seconds/60); + var duration; + + if (minutes < 60) { + duration = [minutes, _lang.mins]; + } else if (minutes == 60) { + duration = ['1', _lang.hr]; + } else { + var hours = (minutes/60).toFixed(1); + if (_lang.decimal != '.') hours = hours.replace('.', _lang.decimal); + duration = [hours, _lang.hrs]; + } + + return duration.join(' '); + }; + + function _int2time(seconds, format) + { + var time = new Date(_baseDate.valueOf() + (seconds*1000)); + var output = ''; + + for (var i=0; i 11) ? 'pm' : 'am'; + break; + + case 'A': + output += (time.getHours() > 11) ? 'PM' : 'AM'; + break; + + case 'g': + var hour = time.getHours() % 12; + output += (hour == 0) ? '12' : hour; + break; + + case 'G': + output += time.getHours(); + break; + + case 'h': + var hour = time.getHours() % 12; + + if (hour != 0 && hour < 10) { + hour = '0'+hour; + } + + output += (hour == 0) ? '12' : hour; + break; + + case 'H': + var hour = time.getHours(); + output += (hour > 9) ? hour : '0'+hour; + break; + + case 'i': + var minutes = time.getMinutes(); + output += (minutes > 9) ? minutes : '0'+minutes; + break; + + case 's': + var seconds = time.getSeconds(); + output += (seconds > 9) ? seconds : '0'+seconds; + break; + + default: + output += code; + } + } + + return output; + }; + + function _time2int(timeString) + { + if (timeString == '') return null; + if (timeString+0 == timeString) return timeString; + + if (typeof(timeString) == 'object') { + timeString = timeString.getHours()+':'+timeString.getMinutes(); + } + + var d = new Date(0); + var time = timeString.toLowerCase().match(/(\d+)(?::(\d\d))?\s*([pa]?)/); + + if (!time) { + return null; + } + + var hour = parseInt(time[1]*1); + + if (time[3]) { + if (hour == 12) { + var hours = (time[3] == 'p') ? 12 : 0; + } else { + var hours = (hour + (time[3] == 'p' ? 12 : 0)); + } + + } else { + var hours = hour; + } + + var minutes = ( time[2]*1 || 0 ); + return hours*3600 + minutes*60; + }; + + // Plugin entry + $.fn.timepicker = function(method) + { + if(methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } + else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); } + else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); } + }; +})(jQuery); \ No newline at end of file diff --git a/common/static/js/vendor/timepicker/jquery.timepicker.min.js b/common/static/js/vendor/timepicker/jquery.timepicker.min.js new file mode 100755 index 0000000000..1678150ab1 --- /dev/null +++ b/common/static/js/vendor/timepicker/jquery.timepicker.min.js @@ -0,0 +1 @@ +!function(e){function o(t){var r=t.data("timepicker-settings"),i=t.data("timepicker-list");i&&i.length&&(i.remove(),t.data("timepicker-list",!1)),i=e("
          "),i.attr("tabindex",-1),i.addClass("ui-timepicker-list"),r.className&&i.addClass(r.className),i.css({display:"none",position:"absolute"}),r.minTime!==null&&r.showDuration&&i.addClass("ui-timepicker-with-duration");var s=r.durationTime!==null?r.durationTime:r.minTime,o=r.minTime!==null?r.minTime:0,u=r.maxTime!==null?r.maxTime:o+n-1;u<=o&&(u+=n);for(var f=o;f<=u;f+=r.step*60){var l=f%n,d=e("
        • ");d.data("time",l),d.text(p(l,r.timeFormat));if(r.minTime!==null&&r.showDuration){var v=e("");v.addClass("ui-timepicker-duration"),v.text(" ("+h(f-s)+")"),d.append(v)}i.append(d)}i.data("timepicker-input",t),t.data("timepicker-list",i),e("body").append(i),a(t,i),i.delegate("li","click",{timepicker:t},function(n){t.addClass("ui-timepicker-hideme"),t[0].focus(),i.find("li").removeClass("ui-timepicker-selected"),e(this).addClass("ui-timepicker-selected"),c(t),i.hide()})}function u(t,n,r){if(!r&&r!==0)return!1;var i=t.data("timepicker-settings"),s=!1;return n.find("li").each(function(t,n){var o=e(n);if(Math.abs(o.data("time")-r)<=i.step*30)return s=o,!1}),s}function a(e,t){var n=d(e.val()),r=u(e,t,n);r&&r.addClass("ui-timepicker-selected")}function f(){if(this.value=="")return;var t=e(this),n=p(d(this.value),t.data("timepicker-settings").timeFormat);t.val(n)}function l(t){var n=e(this),r=n.data("timepicker-list");if(!r.is(":visible")){if(t.keyCode!=40)return!0;n.focus()}switch(t.keyCode){case 13:return c(n),s.hide.apply(this),t.preventDefault(),!1;case 38:var i=r.find(".ui-timepicker-selected");if(!i.length){var i;r.children().each(function(t,n){if(e(n).position().top>0)return i=e(n),!1}),i.addClass("ui-timepicker-selected")}else i.is(":first-child")||(i.removeClass("ui-timepicker-selected"),i.prev().addClass("ui-timepicker-selected"),i.prev().position().top0)return i=e(n),!1}),i.addClass("ui-timepicker-selected")}else i.is(":last-child")||(i.removeClass("ui-timepicker-selected"),i.next().addClass("ui-timepicker-selected"),i.next().position().top+2*i.outerHeight()>r.outerHeight()&&r.scrollTop(r.scrollTop()+i.outerHeight()));break;case 27:r.find("li").removeClass("ui-timepicker-selected"),r.hide();break;case 9:case 16:case 17:case 18:case 19:case 20:case 33:case 34:case 35:case 36:case 37:case 39:case 45:return;default:r.find("li").removeClass("ui-timepicker-selected");return}}function c(e){var t=e.data("timepicker-settings"),n=e.data("timepicker-list"),r=null,i=n.find(".ui-timepicker-selected");if(i.length)var r=i.data("time");else if(e.val()){var r=d(e.val());a(e,n)}if(r!==null){var s=p(r,t.timeFormat);e.attr("value",s)}e.trigger("change").trigger("changeTime")}function h(e){var t=Math.round(e/60),n;if(t<60)n=[t,i.mins];else if(t==60)n=["1",i.hr];else{var r=(t/60).toFixed(1);i.decimal!="."&&(r=r.replace(".",i.decimal)),n=[r,i.hrs]}return n.join(" ")}function p(e,n){var r=new Date(t.valueOf()+e*1e3),i="";for(var s=0;s11?"pm":"am";break;case"A":i+=r.getHours()>11?"PM":"AM";break;case"g":var u=r.getHours()%12;i+=u==0?"12":u;break;case"G":i+=r.getHours();break;case"h":var u=r.getHours()%12;u!=0&&u<10&&(u="0"+u),i+=u==0?"12":u;break;case"H":var u=r.getHours();i+=u>9?u:"0"+u;break;case"i":var a=r.getMinutes();i+=a>9?a:"0"+a;break;case"s":var e=r.getSeconds();i+=e>9?e:"0"+e;break;default:i+=o}}return i}function d(e){if(e=="")return null;if(e+0==e)return e;typeof e=="object"&&(e=e.getHours()+":"+e.getMinutes());var t=new Date(0),n=e.toLowerCase().match(/(\d+)(?::(\d\d))?\s*([pa]?)/);if(!n)return null;var r=parseInt(n[1]*1);if(n[3])if(r==12)var i=n[3]=="p"?12:0;else var i=r+(n[3]=="p"?12:0);else var i=r;var s=n[2]*1||0;return i*3600+s*60}var t=new Date;t.setHours(0),t.setMinutes(0),t.setSeconds(0);var n=86400,r={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1},i={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},s={init:function(t){return this.each(function(){var n=e(this);if(n[0].tagName=="SELECT"){var o=e(""),u={type:"text",value:n.val()},a=n[0].attributes;for(var c=0;ce(window).height()+e(window).scrollTop()?r.css({left:n.offset().left,top:n.offset().top+i-r.outerHeight()}):r.css({left:n.offset().left,top:n.offset().top+i+n.outerHeight()}),r.show();var a=n.data("timepicker-settings"),f=r.find(".ui-timepicker-selected");f.length||(n.val()?f=u(n,r,d(n.val())):a.minTime===null&&a.scrollDefaultNow?f=u(n,r,d(new Date)):a.scrollDefaultTime!==!1&&(f=u(n,r,d(a.scrollDefaultTime))));if(f&&f.length){var l=r.scrollTop()+f.position().top-f.outerHeight();r.scrollTop(l)}else r.scrollTop(0);n.trigger("showTimepicker")},hide:function(t){e(".ui-timepicker-list:visible").each(function(){var t=e(this),n=t.data("timepicker-input"),r=n.data("timepicker-settings");r.selectOnBlur&&c(n),t.hide(),n.trigger("hideTimepicker")})},option:function(t,n){var r=e(this),i=r.data("timepicker-settings"),s=r.data("timepicker-list");if(typeof t=="object")i=e.extend(i,t);else if(typeof t=="string"&&typeof n!="undefined")i[t]=n;else if(typeof t=="string")return i[t];i.minTime&&(i.minTime=d(i.minTime)),i.maxTime&&(i.maxTime=d(i.maxTime)),i.durationTime&&(i.durationTime=d(i.durationTime)),r.data("timepicker-settings",i),s&&(s.remove(),r.data("timepicker-list",!1))},getSecondsFromMidnight:function(){return d(e(this).val())},getTime:function(){return new Date(t.valueOf()+d(e(this).val())*1e3)},setTime:function(t){var n=e(this),r=p(d(t),n.data("timepicker-settings").timeFormat);n.val(r)}};e.fn.timepicker=function(t){if(s[t])return s[t].apply(this,Array.prototype.slice.call(arguments,1));if(typeof t=="object"||!t)return s.init.apply(this,arguments);e.error("Method "+t+" does not exist on jQuery.timepicker")}}(jQuery) \ No newline at end of file From 24247dc0b93205e68305286bd7316366d0371ead Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Fri, 5 Oct 2012 10:29:36 -0400 Subject: [PATCH 0086/1010] cross-browser css3 --- cms/static/sass/_assets.scss | 3 ++- cms/static/sass/_base.scss | 29 ++++++++++++++++------------ cms/static/sass/_cms_mixins.scss | 33 ++++++++++++++++---------------- cms/static/sass/_courseware.scss | 7 ++++--- cms/static/sass/_dashboard.scss | 2 +- cms/static/sass/_header.scss | 10 +++++----- cms/static/sass/_login.scss | 6 +++--- cms/static/sass/_modal.scss | 3 ++- cms/static/sass/_unit.scss | 31 ++++++++++++++++-------------- 9 files changed, 68 insertions(+), 56 deletions(-) diff --git a/cms/static/sass/_assets.scss b/cms/static/sass/_assets.scss index 54ad25c0a3..82df497c9b 100644 --- a/cms/static/sass/_assets.scss +++ b/cms/static/sass/_assets.scss @@ -28,7 +28,8 @@ } thead th { - background: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, .1)) #ced2db; + @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); + background-color: #ced2db; font-size: 12px; font-weight: 700; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 312d066078..008e742bc2 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -16,7 +16,7 @@ body { a { text-decoration: none; color: $blue; - -webkit-transition: color .15s; + @include transition(color .15s); &:hover { color: #cb9c40; @@ -53,7 +53,7 @@ h1 { background: #fff; border: 1px solid $darkGrey; border-radius: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); } .sidebar { @@ -78,24 +78,27 @@ input[type="text"], input[type="email"], input[type="password"] { padding: 6px 8px 8px; - box-sizing: border-box; + @include box-sizing(border-box); border: 1px solid #b0b6c2; border-radius: 2px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)) #edf1f5; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1) inset; + @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)); + background-color: #edf1f5; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); font-family: 'Open Sans', sans-serif; font-size: 11px; color: #3c3c3c; outline: 0; - &::-webkit-input-placeholder { + &::-webkit-input-placeholder, + &:-moz-placeholder, + &:-ms-input-placeholder { color: #979faf; } } input.search { padding: 6px 15px 8px 30px; - box-sizing: border-box; + @include box-sizing(border-box); border: 1px solid $darkGrey; border-radius: 20px; background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5; @@ -116,10 +119,11 @@ label { width: 100%; min-height: 80px; padding: 10px; - box-sizing: border-box; + @include box-sizing(border-box); border: 1px solid #b0b6c2; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3)) #edf1f5; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; + @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3)); + background-color: #edf1f5; + @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset); font-family: Monaco, monospace; } @@ -167,8 +171,9 @@ label { padding: 6px 14px; border-bottom: 1px solid #cbd1db; border-radius: 3px 3px 0 0; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%) #edf1f5; - box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%); + background-color: #edf1f5; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); font-size: 14px; font-weight: 600; } diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index 8ca737f33d..ffc3a0e722 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -13,11 +13,11 @@ padding: 4px 20px 6px; font-size: 14px; font-weight: 700; - box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0); - -webkit-transition: background-color .15s, box-shadow .15s; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0)); + @include transition(background-color .15s, box-shadow .15s); &:hover { - box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15); + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15)); } } @@ -25,7 +25,8 @@ @include button; border: 1px solid #437fbf; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) $blue; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: $blue; color: #fff; &:hover { @@ -38,8 +39,9 @@ @include button; border: 1px solid $darkGrey; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 60%) #dfe5eb; - box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 60%); + background-color: #dfe5eb; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); color: #5d6779; &:hover { @@ -52,8 +54,9 @@ @include button; border: 1px solid #bda046; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%) #edbd3c; - box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + @include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%); + background-color: #edbd3c; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); color: #3c3c3c; &:hover { @@ -66,8 +69,9 @@ @include button; border: 1px solid $darkGrey; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3; - box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: #d1dae3; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); color: #6d788b; &:hover { @@ -80,8 +84,9 @@ padding: 15px 20px; border-radius: 3px; border: 1px solid #5597dd; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)) #5597dd; - box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; + @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); + background-color: #5597dd; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset); label { color: #fff; @@ -165,10 +170,6 @@ .has-new-draft-item { color: #9f7d10; } - - .item-actions { - // display: none; - } } a { diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss index 020548e08a..f135328b62 100644 --- a/cms/static/sass/_courseware.scss +++ b/cms/static/sass/_courseware.scss @@ -22,7 +22,7 @@ input.courseware-unit-search-input { border-radius: 3px; margin: 10px 0; padding-bottom: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); &:first-child { margin-top: 0; @@ -90,7 +90,8 @@ input.courseware-unit-search-input { } .list-header { - background: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, .1)) #ced2db; + @include linear-gradient(top, transparent, rgba(0, 0, 0, .1)); + background-color: #ced2db; border-radius: 3px 3px 0 0; } @@ -129,7 +130,7 @@ input.courseware-unit-search-input { left: 110px; z-index: 9999; border: 1px solid #3C3C3C; - box-shadow: 0 1px 15px rgba(0, 0, 0, .2); + @include box-shadow(0 1px 15px rgba(0, 0, 0, .2)); } .unit-name-input { diff --git a/cms/static/sass/_dashboard.scss b/cms/static/sass/_dashboard.scss index 2abf769923..652d7f9d66 100644 --- a/cms/static/sass/_dashboard.scss +++ b/cms/static/sass/_dashboard.scss @@ -3,7 +3,7 @@ border-radius: 3px; border: 1px solid $darkGrey; background: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); li { border-bottom: 1px solid $mediumGrey; diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index 0fa4f2eddd..84e3ba1588 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -8,10 +8,10 @@ body.no-header { width: 100%; height: 36px; border-bottom: 1px solid #2c2e33; - background: -webkit-linear-gradient(top, #686b76, #54565e); + @include linear-gradient(top, #686b76, #54565e); font-size: 13px; color: #fff; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset); .drop-icon { margin-left: 5px; @@ -38,15 +38,15 @@ body.no-header { } a { - box-shadow: 1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44; + @include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44); &:hover { background: rgba(255, 255, 255, .1); } &.active { - background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3)); - box-shadow: 0 2px 8px rgba(0, 0, 0, .7) inset; + @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3)); + @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset); } } } \ No newline at end of file diff --git a/cms/static/sass/_login.scss b/cms/static/sass/_login.scss index 48216636d0..3cc7551249 100644 --- a/cms/static/sass/_login.scss +++ b/cms/static/sass/_login.scss @@ -8,9 +8,9 @@ height: 36px; border-radius: 3px 3px 0 0; border: 1px solid #2c2e33; - background: -webkit-linear-gradient(top, #686b76, #54565e); + @include linear-gradient(top, #686b76, #54565e); color: #fff; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, .25) inset); h1 { margin: 5px 0; @@ -26,7 +26,7 @@ border-top-width: 0; border-radius: 0 0 3px 3px; background: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); } label { diff --git a/cms/static/sass/_modal.scss b/cms/static/sass/_modal.scss index a60d5629e3..854d1e1045 100644 --- a/cms/static/sass/_modal.scss +++ b/cms/static/sass/_modal.scss @@ -28,7 +28,8 @@ .modal-actions { height: 60px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)) #d1dae3; + @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); + background-color: #d1dae3; } h2 { diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 0cc855a56f..2062548b08 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -22,8 +22,9 @@ .breadcrumbs { border-radius: 3px 3px 0 0; border-bottom: 1px solid #cbd1db; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%) #edf1f5; - box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%); + background-color: #edf1f5; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); @include clearfix; li { @@ -56,7 +57,7 @@ border: 1px solid #d1ddec; border-radius: 3px; background: #fff; - -webkit-transition: border-color .15s; + @include transition(border-color .15s); &:hover { border-color: #6696d7; @@ -94,7 +95,7 @@ position: absolute; top: 4px; right: 4px; - -webkit-transition: opacity .15s; + @include transition(opacity .15s); } .edit-button, @@ -107,7 +108,7 @@ background: #d1ddec; font-size: 12px; color: #fff; - -webkit-transition: all .15s; + @include transition(all .15s); &:hover { background-color: $blue; @@ -132,16 +133,17 @@ border: 1px solid #d1ddec; background: url(../img/drag-handles.png) center no-repeat #d1ddec; cursor: move; - -webkit-transition: all .15s; + @include transition(all .15s); } &.new-component-item { padding: 0; border: 1px solid #8891a1; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3; - box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; - -webkit-transition: background-color .15s, border-color .15s; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: #d1dae3; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset); + @include transition(background-color .15s, border-color .15s); &.adding { background-color: $blue; @@ -182,8 +184,8 @@ line-height: 14px; color: #fff; text-align: center; - box-shadow: 0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset; - -webkit-transition: background-color .15s; + @include box-shadow(0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset); + @include transition(background-color .15s); &:hover { background-color: rgba(255, 255, 255, .2); @@ -195,7 +197,7 @@ left: 0; width: 100%; padding: 10px; - box-sizing: border-box; + @include box-sizing(border-box); } } } @@ -225,7 +227,8 @@ display: none; padding: 20px; border-radius: 0 0 3px 3px; - background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)) $blue; + @include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)); + background-color: $blue; color: #fff; .metadata_edit { @@ -360,7 +363,7 @@ .url { width: 100%; margin-bottom: 10px; - box-shadow: none; + @include box-shadow(none); } .window-contents > ol { From 95e15c4455ba7da281f87a0c1060344c80dc5d85 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 13:17:51 -0400 Subject: [PATCH 0087/1010] time/date setting on Release Date and Due date --- cms/djangoapps/contentstore/views.py | 15 +++++++++++- cms/static/js/base.js | 34 +++++++++++++++++++++++++-- cms/templates/edit_subsection.html | 35 +++++++++++++++++++--------- cms/templates/widgets/units.html | 2 +- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index fe9204c21a..df2a6fd986 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -440,15 +440,28 @@ def save_item(request): # cdodge: also commit any metadata which might have been passed along in the # POST from the client, if it is there - # note, that the postback is not the complete metadata, as there's system metadata which is + # NOTE, that the postback is not the complete metadata, as there's system metadata which is # not presented to the end-user for editing. So let's fetch the original and # 'apply' the submitted metadata, so we don't end up deleting system metadata if request.POST['metadata']: posted_metadata = request.POST['metadata'] # fetch original existing_item = modulestore().get_item(item_location) + + logging.debug(posted_metadata) + # update existing metadata with submitted metadata (which can be partial) + # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' + for metadata_key in posted_metadata.keys(): + if posted_metadata[metadata_key] is None: + # remove both from passed in collection as well as the collection read in from the modulestore + del existing_item.metadata[metadata_key] + del posted_metadata[metadata_key] + + # overlay the new metadata over the modulestore sourced collection to support partial updates existing_item.metadata.update(posted_metadata) + + # commit to datastore modulestore().update_metadata(item_location, existing_item.metadata) return HttpResponse() diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 3508c80aed..bc637b0075 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -60,6 +60,27 @@ function onUnitReordered() { }); } +function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { + var input_date = $('#'+date_id).val(); + var input_time = $('#'+time_id).val(); + + var edxTimeStr = null; + + if (input_date != '') { + if (input_time == '') + input_time = '00:00'; + + // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing + date = Date.parse(input_date+" "+input_time); + if (format == null) + format = 'yyyy-MM-ddTHH:mm'; + + edxTimeStr = date.toString(format); + } + + return edxTimeStr; +} + function saveSubsection(e) { e.preventDefault(); @@ -70,10 +91,19 @@ function saveSubsection(e) { metadata = {}; for(var i=0; i< metadata_fields.length;i++) { - el = metadata_fields[i]; - metadata[$(el).data("metadata-name")] = el.value; + el = metadata_fields[i]; + metadata[$(el).data("metadata-name")] = el.value; } + // OK, we have some metadata (namely 'Release Date' (aka 'start') and 'Due Date') which has been normalized in the UI + // we have to piece it back together. Unfortunate 'start' and 'due' use different string formatters. Rather than try to + // replicate the string formatting which is used in the backend here in JS, let's just pass back a unified format + // and let the server re-format into the expected persisted format + + metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time'); + metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm'); + + // reordering is done through immediate callbacks when the resorting has completed in the UI children =[]; $.ajax({ diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index b05c2121fc..3fa1e135dd 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,10 +1,9 @@ <%inherit file="base.html" /> <%! - import time + from time import mktime import dateutil.parser + import logging from datetime import datetime - - now = datetime.now() %> <%! from django.core.urlresolvers import reverse %> @@ -32,7 +31,7 @@ ${units.enum_units(subsection)} -
          +
          @@ -46,10 +45,13 @@
          - - + <% + start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None + %> + +
          -

          The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.

          +

          The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.

          @@ -57,10 +59,11 @@

          <% - due_date = dateutil.parser.parse(subsection.metadata.get('get')) if 'due' in subsection.metadata else None - %> - - + # due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use + due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None + %> + + Remove due date

          @@ -79,4 +82,14 @@ + + diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index 8207b485a7..67e956561c 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -17,7 +17,7 @@ This def will enumerate through a passed in subsection and list all of the units ${unit.display_name} - - private + - private % if actions:
          From d55769861eb3dadf823784b0388b1f4239200732 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 14:45:07 -0400 Subject: [PATCH 0088/1010] check if 'delete metadata field' is not in the list of system metadata --- cms/djangoapps/contentstore/views.py | 9 +++++---- cms/static/js/base.js | 7 +++---- common/static/js/vendor/timepicker/datepair.js | 12 ++++++++++++ .../static/js/vendor/timepicker/jquery.timepicker.js | 2 ++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index df2a6fd986..c1c3fc92d0 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -448,14 +448,15 @@ def save_item(request): # fetch original existing_item = modulestore().get_item(item_location) - logging.debug(posted_metadata) - # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' for metadata_key in posted_metadata.keys(): - if posted_metadata[metadata_key] is None: + # NOTE: We don't want clients to be able to delete 'system metadata' which are not intended to be user + # editable + if posted_metadata[metadata_key] is None and metadata_key not in existing_item.system_metadata_fields: # remove both from passed in collection as well as the collection read in from the modulestore - del existing_item.metadata[metadata_key] + if metadata_key in existing_item.metadata: + del existing_item.metadata[metadata_key] del posted_metadata[metadata_key] # overlay the new metadata over the modulestore sourced collection to support partial updates diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bc637b0075..06e8f33c92 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -95,11 +95,10 @@ function saveSubsection(e) { metadata[$(el).data("metadata-name")] = el.value; } - // OK, we have some metadata (namely 'Release Date' (aka 'start') and 'Due Date') which has been normalized in the UI - // we have to piece it back together. Unfortunate 'start' and 'due' use different string formatters. Rather than try to - // replicate the string formatting which is used in the backend here in JS, let's just pass back a unified format - // and let the server re-format into the expected persisted format + // Piece back together the date/time UI elements into one date/time string + // NOTE: our various "date/time" metadata elements don't always utilize the same formatting string + // so make sure we're passing back the correct format metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time'); metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm'); diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js index d547925e5b..f210933593 100644 --- a/common/static/js/vendor/timepicker/datepair.js +++ b/common/static/js/vendor/timepicker/datepair.js @@ -1,3 +1,15 @@ +/************************ +datepair.js + +This is a component of the jquery-timepicker plugin + +http://jonthornton.github.com/jquery-timepicker/ + +requires jQuery 1.6+ + +version: 1.2.2 +************************/ + $(function() { $('.datepair input.date').each(function(){ diff --git a/common/static/js/vendor/timepicker/jquery.timepicker.js b/common/static/js/vendor/timepicker/jquery.timepicker.js index 506f66b5d1..3a462dd436 100755 --- a/common/static/js/vendor/timepicker/jquery.timepicker.js +++ b/common/static/js/vendor/timepicker/jquery.timepicker.js @@ -3,6 +3,8 @@ jquery-timepicker http://jonthornton.github.com/jquery-timepicker/ requires jQuery 1.6+ + +version: 1.2.2 ************************/ From 0cf6a1179a6730832802b1d48c093b36db33f398 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 8 Oct 2012 10:43:46 -0400 Subject: [PATCH 0089/1010] wip - trigger display message when release data is different than parent release date --- cms/djangoapps/contentstore/views.py | 21 ++++-- cms/templates/edit_subsection.html | 13 ++-- common/static/js/vendor/date.js | 104 +++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 common/static/js/vendor/date.js diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index c1c3fc92d0..956f9c1695 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -149,12 +149,20 @@ def edit_subsection(request, location): if item.location.category != 'sequential': return HttpResponseBadRequest - logging.debug('Start = {0}'.format(item.start)) + parent_locs = modulestore().get_parent_locations(location) + + # we're for now assuming a single parent + if len(parent_locs) != 1: + logging.error('Multiple (or none) parents have been found for {0}'.format(location)) + + # this should blow up if we don't find any parents, which would be erroneous + parent = modulestore().get_item(parent_locs[0]) return render_to_response('edit_subsection.html', {'subsection': item, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), - 'lms_link': lms_link + 'lms_link': lms_link, + 'parent_item' : parent }) @login_required @@ -451,9 +459,12 @@ def save_item(request): # update existing metadata with submitted metadata (which can be partial) # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' for metadata_key in posted_metadata.keys(): - # NOTE: We don't want clients to be able to delete 'system metadata' which are not intended to be user - # editable - if posted_metadata[metadata_key] is None and metadata_key not in existing_item.system_metadata_fields: + + # let's strip out any metadata fields from the postback which have been identified as system metadata + # and therefore should not be user-editable, so we should accept them back from the client + if metadata_key in existing_item.system_metadata_fields: + del posted_metadata[metadata_key] + elif posted_metadata[metadata_key] is None: # remove both from passed in collection as well as the collection read in from the modulestore if metadata_key in existing_item.metadata: del existing_item.metadata[metadata_key] diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 3fa1e135dd..2201b06b14 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -46,12 +46,15 @@
          <% - start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None + start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None + parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None %> - - + +
          -

          The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.

          + % if start_date != parent_start_date and parent_start_date is not None: +

          The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. Sync to ${parent_item.display_name}.

          + % endif
          @@ -62,7 +65,7 @@ # due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None %> - + Remove due date

          diff --git a/common/static/js/vendor/date.js b/common/static/js/vendor/date.js new file mode 100644 index 0000000000..77f498645c --- /dev/null +++ b/common/static/js/vendor/date.js @@ -0,0 +1,104 @@ +/** + * Version: 1.0 Alpha-1 + * Build Date: 13-Nov-2007 + * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ + */ +Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}}; +Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;idate)?1:(this=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;} +var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);} +if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);} +if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);} +if(x.hour||x.hours){this.addHours(x.hour||x.hours);} +if(x.month||x.months){this.addMonths(x.month||x.months);} +if(x.year||x.years){this.addYears(x.year||x.years);} +if(x.day||x.days){this.addDays(x.day||x.days);} +return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(valuemax){throw new RangeError(value+" is not a valid value for "+name+".");} +return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;} +if(!x.second&&x.second!==0){x.second=-1;} +if(!x.minute&&x.minute!==0){x.minute=-1;} +if(!x.hour&&x.hour!==0){x.hour=-1;} +if(!x.day&&x.day!==0){x.day=-1;} +if(!x.month&&x.month!==0){x.month=-1;} +if(!x.year&&x.year!==0){x.year=-1;} +if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());} +if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());} +if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());} +if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());} +if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());} +if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());} +if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());} +if(x.timezone){this.setTimezone(x.timezone);} +if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);} +return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;} +var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}} +return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();}; +Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;} +return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;} +if(!last&&q[1].length===0){last=true;} +if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}} +if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];} +if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");} +var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});} +return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;} +for(var i=0;i Date: Mon, 8 Oct 2012 16:46:25 -0400 Subject: [PATCH 0090/1010] allow for adding/deleting of new 'policy' metadata in the edit subsection page --- cms/djangoapps/contentstore/views.py | 10 +++++++- cms/static/js/base.js | 38 +++++++++++++++++++++++++++- cms/templates/edit_subsection.html | 27 +++++++++++++++----- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 956f9c1695..622d29772b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -158,11 +158,19 @@ def edit_subsection(request, location): # this should blow up if we don't find any parents, which would be erroneous parent = modulestore().get_item(parent_locs[0]) + # remove all metadata from the generic dictionary that is presented in a more normalized UI + + policy_metadata = dict((key,value) for key, value in item.metadata.iteritems() + if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) + + logging.debug(policy_metadata) + return render_to_response('edit_subsection.html', {'subsection': item, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'lms_link': lms_link, - 'parent_item' : parent + 'parent_item' : parent, + 'policy_metadata' : policy_metadata }) @login_required diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 06e8f33c92..13fcda55b1 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -36,8 +36,31 @@ $(document).ready(function() { $('.set-date').bind('click', showDateSetter); $('.remove-date').bind('click', removeDateSetter); + // add/remove policy metadata button click handlers + $('.add-policy-data').bind('click', addPolicyMetadata); + $('.remove-policy-data').bind('click', removePolicyMetadata); + }); +function addPolicyMetadata(e) { + e.preventDefault(); + var template =$('#add-new-policy-element-template > li'); + var newNode = template.clone(); + var _parent_el = $(this).parent('ol:.policy-list'); + newNode.insertBefore('.add-policy-data'); + $('.remove-policy-data').bind('click', removePolicyMetadata); +} + +function removePolicyMetadata(e) { + e.preventDefault(); + policy_name = $(this).data('policy-name'); + var _parent_el = $(this).parent('li:.policy-list-element'); + //$(_parent_el).remove(); + + _parent_el.appendTo("#policy-to-delete"); +} + + // This method only changes the ordering of the child objects in a subsection function onUnitReordered() { var subsection_id = $(this).data('subsection-id'); @@ -86,7 +109,7 @@ function saveSubsection(e) { var id = $(this).data('id'); - // pull all metadata editable fields on page + // pull all 'normalized' metadata editable fields on page var metadata_fields = $('input[data-metadata-name]'); metadata = {}; @@ -95,6 +118,19 @@ function saveSubsection(e) { metadata[$(el).data("metadata-name")] = el.value; } + // now add 'free-formed' metadata which are presented to the user as dual input fields (name/value) + $('ol.policy-list > li.policy-list-element').each( function(i, element) { + name = $(element).children('.policy-list-name').val(); + val = $(element).children('.policy-list-value').val(); + metadata[name] = val; + }); + + // now add any 'removed' policy metadata which is stored in a separate hidden div + // 'null' presented to the server means 'remove' + $("#policy-to-delete > li.policy-list-element").each(function(i, element) { + name = $(element).children('.policy-list-name').val(); + metadata[name] = null; + }); // Piece back together the date/time UI elements into one date/time string // NOTE: our various "date/time" metadata elements don't always utilize the same formatting string diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 2201b06b14..6f2347398b 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -25,7 +25,7 @@
          - +
          @@ -33,11 +33,25 @@
          - +
            + % for policy_name in policy_metadata.keys(): +
          1. + +
          2. + % endfor + Add +
          + + +
        • + + @@ -88,6 +102,7 @@ + +<%block name="jsextra"> + + + \ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index 44f42343f3..890e9e2671 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -22,10 +22,11 @@ urlpatterns = ('', 'contentstore.views.preview_dispatch', name='preview_dispatch'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/upload_asset$', 'contentstore.views.upload_asset', name='upload_asset'), - url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/manage_users$', - 'contentstore.views.manage_users', name='manage_users'), - url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/add_user$', + url(r'^manage_users/(?P.*?)$', 'contentstore.views.manage_users', name='manage_users'), + url(r'^add_user/(?P.*?)$', 'contentstore.views.add_user', name='add_user'), + url(r'^remove_user/(?P.*?)$', + 'contentstore.views.remove_user', name='remove_user'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/remove_user$', 'contentstore.views.remove_user', name='remove_user'), url(r'^assets/(?P.*?)$', 'contentstore.views.asset_index', name='asset_index'), From eb2fadb639478ce6c349075a884cf29fc569ec07 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 14:45:56 -0400 Subject: [PATCH 0094/1010] added static page templates; urls will probably need some reworking, but i wanted to get the templates in there --- cms/djangoapps/contentstore/views.py | 6 ++++++ cms/urls.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index c1c3fc92d0..58f0eec5ad 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -675,3 +675,9 @@ def asset_index(request, location): # points to the temporary course landing page with log in and sign up def landing(request, org, course, coursename): return render_to_response('temp-course-landing.html', {}) + +def static_pages(request, org, course, coursename): + return render_to_response('static-pages.html', {}) + +def edit_static(request, org, course, coursename): + return render_to_response('edit-static-page.html', {}) \ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index 44f42343f3..324b3d554d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -29,6 +29,8 @@ urlpatterns = ('', url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)/remove_user$', 'contentstore.views.remove_user', name='remove_user'), url(r'^assets/(?P.*?)$', 'contentstore.views.asset_index', name='asset_index'), + url(r'^pages/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.static_pages', name='static_pages'), + url(r'^edit_static/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_static', name='edit_static'), # temporary landing page for a course url(r'^landing/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.landing', name='landing') From 4c50f13e9944abe5c249d0f896702f2d486be408 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 14:47:15 -0400 Subject: [PATCH 0095/1010] added templates/styles for static pages --- cms/static/sass/_static-pages.scss | 64 +++++++++++++++++++++++++++++ cms/static/sass/base-style.scss | 1 + cms/templates/edit-static-page.html | 41 ++++++++++++++++++ cms/templates/static-pages.html | 41 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 cms/static/sass/_static-pages.scss create mode 100644 cms/templates/edit-static-page.html create mode 100644 cms/templates/static-pages.html diff --git a/cms/static/sass/_static-pages.scss b/cms/static/sass/_static-pages.scss new file mode 100644 index 0000000000..628d537f90 --- /dev/null +++ b/cms/static/sass/_static-pages.scss @@ -0,0 +1,64 @@ +.static-pages { + .new-static-page-button { + @include grey-button; + display: block; + text-align: center; + padding: 12px 0; + } + + .static-page-item { + position: relative; + margin: 10px 0; + padding: 22px 20px; + border: 1px solid $darkGrey; + border-radius: 3px; + background: #fff; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1)); + + .page-name { + font-size: 19px; + font-weight: 700; + } + + .item-actions { + margin-top: 19px; + margin-right: 12px; + } + } +} + +.edit-static-page { + .main-wrapper { + margin-top: 40px; + } + + .static-page-details { + @extend .window; + padding: 32px 40px; + + .row { + border: none; + } + } + + .page-display-name-input { + width: 100%; + font-size: 20px; + } + + .page-contents { + @include box-sizing(border-box); + width: 100%; + height: 360px; + padding: 15px; + border: 1px solid #b0b6c2; + border-radius: 2px; + @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3)); + background-color: #edf1f5; + @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset); + font-family: Monaco, monospace; + font-size: 13px; + color: #3c3c3c; + outline: 0; + } +} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 8743790bba..628332d9ee 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -14,6 +14,7 @@ @import "subsection"; @import "unit"; @import "assets"; +@import "static-pages"; @import "course-info"; @import "landing"; @import "graphics"; diff --git a/cms/templates/edit-static-page.html b/cms/templates/edit-static-page.html new file mode 100644 index 0000000000..f86d00989e --- /dev/null +++ b/cms/templates/edit-static-page.html @@ -0,0 +1,41 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Edit Static Page +<%block name="bodyclass">edit-static-page + +<%block name="content"> +
          +
          +
          +
          +
          + + +
          +
          + + +
          +
          +
          + +
          +
          + \ No newline at end of file diff --git a/cms/templates/static-pages.html b/cms/templates/static-pages.html new file mode 100644 index 0000000000..67945f0832 --- /dev/null +++ b/cms/templates/static-pages.html @@ -0,0 +1,41 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Static Pages +<%block name="bodyclass">static-pages + +<%block name="content"> +
          +
          +

          Static Pages

          +
          + +
          + +
          +
          + \ No newline at end of file From d65e1876a0f6cb8b9ae44a333452f23d48309f0b Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 14:48:02 -0400 Subject: [PATCH 0096/1010] tweaked items in the header; tried creating class drop, but can't get the server-side jazz figured out --- cms/templates/widgets/header.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 21e0b002cb..0805354b85 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -3,12 +3,13 @@
        From a80e8ce3d54e88320679b7a535ba894027ca6ff4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 9 Oct 2012 15:11:07 -0400 Subject: [PATCH 0097/1010] respond to Cale pull request comments. Update access.py (LMS) to use the same 'check for legacy group name first' logic. Add SECRET_KEY setting to the CAS project so that auth tokens from CAS are accepted in the LMS --- cms/djangoapps/auth/authz.py | 9 ++++++++- cms/envs/dev.py | 3 +++ cms/templates/manage_users.html | 16 ---------------- lms/djangoapps/courseware/access.py | 25 +++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 90c51e45a2..659588f6f6 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -20,7 +20,14 @@ STAFF_ROLE_NAME = 'staff' # of those two variables def get_course_groupname_for_role(location, role): loc = Location(location) - groupname = role + '_' + loc.course + # hack: check for existence of a group name in the legacy LMS format _ + # if it exists, then use that one, otherwise use a _ which contains + # more information + groupname = '{0}_{1}'.format(role, loc.course) + + if len(Group.objects.filter(name = groupname)) == 0: + groupname = '{0}_{1}'.format(role,loc.course_id) + return groupname def get_users_in_course_group_by_role(location, role): diff --git a/cms/envs/dev.py b/cms/envs/dev.py index d692d202fd..8401fa5c15 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -97,3 +97,6 @@ CACHES = { # Make the keyedcache startup warnings go away CACHE_TIMEOUT = 0 + +# Dummy secret key for dev +SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index b525080123..b647d629b8 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -27,22 +27,6 @@
        - - diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 2612efedb9..4d49684ffe 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -6,6 +6,7 @@ import logging import time from django.conf import settings +from django.contrib.auth.models import Group from xmodule.course_module import CourseDescriptor from xmodule.error_module import ErrorDescriptor @@ -306,13 +307,25 @@ def _dispatch(table, action, user, obj): raise ValueError("Unknown action for object type '{0}': '{1}'".format( type(obj), action)) + +def _does_course_group_name_exist(name): + return len(Group.objects.filter(name = name)) > 0 + def _course_staff_group_name(location): """ Get the name of the staff group for a location. Right now, that's staff_COURSE. location: something that can passed to Location. + + cdodge: We're changing the name convention of the group to better epxress different runs of courses by + using course_id rather than just the course number. So first check to see if the group name exists """ - return 'staff_%s' % Location(location).course + loc = Location(location) + legacy_name = 'staff_%s' % loc.course + if _does_course_group_name_exist(legacy_name): + return legacy_name + + return 'staff_%s' & loc.course_id def _course_instructor_group_name(location): @@ -321,8 +334,16 @@ def _course_instructor_group_name(location): A course instructor has all staff privileges, but also can manage list of course staff (add, remove, list). location: something that can passed to Location. + + cdodge: We're changing the name convention of the group to better epxress different runs of courses by + using course_id rather than just the course number. So first check to see if the group name exists """ - return 'instructor_%s' % Location(location).course + loc = Location(location) + legacy_name = 'instructor_%s' % loc.course + if _does_course_group_name_exist(legacy_name): + return legacy_name + + return 'instructor_%s' % loc.course_id def _has_global_staff_access(user): From 2441f8960ef1a13546e9cbd6fd727753e5f67229 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 15:26:12 -0400 Subject: [PATCH 0098/1010] added waiting classes --- cms/static/img/blue-spinner.gif | Bin 0 -> 8181 bytes cms/templates/users.html | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 cms/static/img/blue-spinner.gif create mode 100644 cms/templates/users.html diff --git a/cms/static/img/blue-spinner.gif b/cms/static/img/blue-spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..2cee72553af43e9b3c606bc7dbd207b05e8ac94c GIT binary patch literal 8181 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 +<%! from django.core.urlresolvers import reverse %> +<%block name="title">CMS Courseware Overview + +<%namespace name="units" file="widgets/units.html" /> + + +<%block name="content"> +
        +
        +

        Courseware

        +
        + +
        + +
        + <%include file="widgets/upload_assets.html"/> +
        +
        + From 002f813b7518bd5cce5a248915c3c9b49520d9d6 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 15:28:53 -0400 Subject: [PATCH 0099/1010] pulled spinner out into its own class in case we need to add it directly --- cms/static/sass/_base.scss | 36 ++++++++++++++++++++++++++++++++++ cms/static/sass/_graphics.scss | 9 +++++++++ 2 files changed, 45 insertions(+) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 27cd5e3a96..fe97c9b975 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -279,4 +279,40 @@ body.show-wip { font-size: 12px; text-align: center; } +} + +.waiting { + position: relative; + + &:before { + content: ''; + display: block; + position: absolute; + top: 0; + left: 0; + z-index: 999998; + width: 100%; + height: 100%; + border-radius: inherit; + background: rgba(255, 255, 255, .9); + } + + &:after { + content: ''; + @extend .spinner-icon; + display: block; + position: absolute; + top: 50%; + left: 50%; + margin-left: -10px; + margin-top: -10px; + z-index: 999999; + } +} + +.waiting-inline { + &:after { + content: ''; + @extend .spinner-icon; + } } \ No newline at end of file diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/_graphics.scss index 14662c7d42..65c827981a 100644 --- a/cms/static/sass/_graphics.scss +++ b/cms/static/sass/_graphics.scss @@ -236,3 +236,12 @@ margin-right: 5px; background: url(../img/large-video-icon.png) center no-repeat; } + +.spinner-icon { + display: inline-block; + width: 20px; + height: 20px; + margin-left: 10px; + vertical-align: middle; + background: url(../img/blue-spinner.gif) no-repeat; +} \ No newline at end of file From 9cb6331c062ebb88ebf9a868ae1da89d491aa99f Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 15:32:23 -0400 Subject: [PATCH 0100/1010] removed users template --- cms/templates/users.html | 64 ---------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 cms/templates/users.html diff --git a/cms/templates/users.html b/cms/templates/users.html deleted file mode 100644 index 3282d483bf..0000000000 --- a/cms/templates/users.html +++ /dev/null @@ -1,64 +0,0 @@ -<%inherit file="base.html" /> -<%! from django.core.urlresolvers import reverse %> -<%block name="title">CMS Courseware Overview - -<%namespace name="units" file="widgets/units.html" /> - - -<%block name="content"> -
        -
        -

        Courseware

        -
        - -
        - -
        - <%include file="widgets/upload_assets.html"/> -
        -
        - From 6d7fff1f50dd66269d384716cbdbfacd2fe643a4 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 9 Oct 2012 16:55:32 -0400 Subject: [PATCH 0101/1010] fixed occassional stacking glitch on header --- cms/static/sass/_header.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index 84e3ba1588..d70f53b4df 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -13,6 +13,10 @@ body.no-header { color: #fff; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset); + .left { + width: 700px; + } + .drop-icon { margin-left: 5px; font-size: 11px; From 5fd912baa9edb46d77fecdeb53b9f2484f475d82 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 10 Oct 2012 09:40:51 -0400 Subject: [PATCH 0102/1010] moved component editor to top; polished editor styles --- cms/static/sass/_unit.scss | 25 ++++++++++++++---------- cms/templates/component.html | 13 ++++++------ cms/templates/widgets/metadata-edit.html | 2 +- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 7ed7e3ee7e..e8abf56190 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -75,15 +75,9 @@ &.editing { border-color: #6696d7; - &:hover { - .drag-handle, - .component-actions a { - background-color: $blue; - } - - .drag-handle { - border-color: $blue; - } + .drag-handle, + .component-actions { + display: none; } } @@ -230,14 +224,24 @@ @include edit-box; display: none; padding: 20px; - border-radius: 0 0 3px 3px; + border-radius: 2px 2px 0 0; @include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)); background-color: $blue; color: #fff; + @include box-shadow(none); .metadata_edit { margin-bottom: 20px; font-size: 13px; + + li { + margin-bottom: 10px; + } + + label { + display: inline-block; + margin-right: 10px; + } } .CodeMirror { @@ -245,6 +249,7 @@ } h3 { + margin-bottom: 10px; font-size: 18px; font-weight: 700; } diff --git a/cms/templates/component.html b/cms/templates/component.html index f0d266e2e1..648a5e3d98 100644 --- a/cms/templates/component.html +++ b/cms/templates/component.html @@ -1,9 +1,3 @@ -${preview} -
        - Edit - Delete -
        -
        ${editor} @@ -11,4 +5,9 @@ ${preview} Save Cancel
        - +
        + Edit + Delete +
        + +${preview} \ No newline at end of file diff --git a/cms/templates/widgets/metadata-edit.html b/cms/templates/widgets/metadata-edit.html index 62d5563047..f960ecfebd 100644 --- a/cms/templates/widgets/metadata-edit.html +++ b/cms/templates/widgets/metadata-edit.html @@ -3,7 +3,7 @@

        Metadata

          % for keyname in editable_metadata_fields: -
        • ${keyname}:
        • +
        • % endfor
        From 75f8b7c98d17d542c007da56114a6d984faec42b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 10:44:18 -0400 Subject: [PATCH 0103/1010] Add Draft module store that is used whenever any item is update in the CAS (but not during import, and not for templates) --- .../management/commands/import.py | 2 +- cms/envs/dev.py | 22 +-- .../xmodule/xmodule/modulestore/__init__.py | 4 +- .../lib/xmodule/xmodule/modulestore/draft.py | 128 ++++++++++++++++++ .../lib/xmodule/xmodule/modulestore/mongo.py | 6 + common/lib/xmodule/xmodule/templates.py | 6 +- 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 common/lib/xmodule/xmodule/modulestore/draft.py diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1d15f1e7df..a8ec2c2685 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -26,4 +26,4 @@ class Command(BaseCommand): print "Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, courses=course_dirs) - import_from_xml(modulestore(), data_dir, course_dirs, load_error_modules=False) + import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 8401fa5c15..e5548df2d4 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log", tracking_filename="tracking.log", debug=True) +modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'xmodule', + 'collection': 'modulestore', + 'fs_root': GITHUB_REPO_ROOT, + 'render_template': 'mitxmako.shortcuts.render_to_string', +} + MODULESTORE = { 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'xmodule', - 'collection': 'modulestore', - 'fs_root': GITHUB_REPO_ROOT, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } + 'OPTIONS': modulestore_options } } diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 880159d8ed..3ee83449f9 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -345,7 +345,9 @@ class ModuleStore(object): Returns a list containing the top level XModuleDescriptors of the courses in this modulestore. ''' - raise NotImplementedError + # TODO (vshnayder): Why do I have to specify i4x here? + course_filter = Location("i4x", category="course") + return self.get_items(course_filter) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py new file mode 100644 index 0000000000..6293063ffa --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -0,0 +1,128 @@ + +from . import ModuleStoreBase, Location +from .exceptions import ItemNotFoundError + + +class DraftModuleStore(ModuleStoreBase): + """ + This mixin modifies a modulestore to give it draft semantics. + That is, edits made to units are stored to locations that have the revision 'draft', + and when reads are made, they first read with revision 'draft', and then fall back + to the baseline revision only if 'draft' doesn't exist. + + This module also includes functionality to promote 'draft' modules (and optionally + their children) to published modules. + """ + + def get_item(self, location, depth=0): + """ + Returns an XModuleDescriptor instance for the item at location. + If location.revision is None, returns the item with the most + recent revision + + If any segment of the location is None except revision, raises + xmodule.modulestore.exceptions.InsufficientSpecificationError + + If no object is found at that location, raises + xmodule.modulestore.exceptions.ItemNotFoundError + + location: Something that can be passed to Location + + depth (int): An argument that some module stores may use to prefetch + descendents of the queried modules for more efficient results later + in the request. The depth is counted in the number of calls to + get_children() to cache. None indicates to cache all descendents + """ + try: + return super(DraftModuleStore, self).get_item(Location(location)._replace(revision='draft'), depth) + except ItemNotFoundError: + return super(DraftModuleStore, self).get_item(location, depth) + + def get_instance(self, course_id, location): + """ + Get an instance of this location, with policy for course_id applied. + TODO (vshnayder): this may want to live outside the modulestore eventually + """ + try: + return super(DraftModuleStore, self).get_instance(course_id, Location(location)._replace(revision='draft')) + except ItemNotFoundError: + return super(DraftModuleStore, self).get_instance(course_id, location) + + def get_items(self, location, depth=0): + """ + Returns a list of XModuleDescriptor instances for the items + that match location. Any element of location that is None is treated + as a wildcard that matches any value + + location: Something that can be passed to Location + + depth: An argument that some module stores may use to prefetch + descendents of the queried modules for more efficient results later + in the request. The depth is counted in the number of calls to + get_children() to cache. None indicates to cache all descendents + """ + draft_loc = Location(location)._replace(revision='draft') + draft_items = super(DraftModuleStore, self).get_items(draft_loc, depth) + items = super(DraftModuleStore, self).get_items(location, depth) + + draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) + non_draft_items = [ + item + for item in items + if (item.location.revision != 'draft' + and item.location._replace(revision=None) not in draft_locs_found) + ] + return draft_items + non_draft_items + + def clone_item(self, source, location): + """ + Clone a new item that is a copy of the item at the location `source` + and writes it to `location` + """ + return super(DraftModuleStore, self).clone_item(source, Location(location)._replace(revision='draft')) + + def update_item(self, location, data): + """ + Set the data in the item specified by the location to + data + + location: Something that can be passed to Location + data: A nested dictionary of problem data + """ + return super(DraftModuleStore, self).update_item(Location(location)._replace(revision='draft'), data) + + def update_children(self, location, children): + """ + Set the children for the item specified by the location to + children + + location: Something that can be passed to Location + children: A list of child item identifiers + """ + return super(DraftModuleStore, self).update_children(Location(location)._replace(revision='draft'), children) + + def update_metadata(self, location, metadata): + """ + Set the metadata for the item specified by the location to + metadata + + location: Something that can be passed to Location + metadata: A nested dictionary of module metadata + """ + return super(DraftModuleStore, self).update_metadata(Location(location)._replace(revision='draft'), metadata) + + def delete_item(self, location): + """ + Delete an item from this modulestore + + location: Something that can be passed to Location + """ + return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision='draft')) + + def get_parent_locations(self, location): + '''Find all locations that are the parents of this location. Needed + for path_to_location(). + + returns an iterable of things that can be passed to Location. + ''' + return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision='draft')) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 21e28e9d67..1e203c6a78 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -13,6 +13,7 @@ from xmodule.mako_module import MakoDescriptorSystem from xmodule.error_module import ErrorDescriptor from . import ModuleStoreBase, Location +from .draft import DraftModuleStore from .exceptions import (ItemNotFoundError, DuplicateItemError) @@ -341,3 +342,8 @@ class MongoModuleStore(ModuleStoreBase): are loaded on demand, rather than up front """ return {} + + +# DraftModuleStore is first, because it needs to intercept calls to MongoModuleStore +class DraftMongoModuleStore(DraftModuleStore, MongoModuleStore): + pass diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py index 2937cddea6..41b1523709 100644 --- a/common/lib/xmodule/xmodule/templates.py +++ b/common/lib/xmodule/xmodule/templates.py @@ -75,6 +75,6 @@ def update_templates(): ), exc_info=True) continue - modulestore().update_item(template_location, template.data) - modulestore().update_children(template_location, template.children) - modulestore().update_metadata(template_location, template.metadata) + modulestore('direct').update_item(template_location, template.data) + modulestore('direct').update_children(template_location, template.children) + modulestore('direct').update_metadata(template_location, template.metadata) From 8da372344542af5a484a3b6f2843dbc4093c13a0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 11:22:52 -0400 Subject: [PATCH 0104/1010] Clone the full item as a draft if it doesn't already exist when doing a draft editing operation --- .../lib/xmodule/xmodule/modulestore/draft.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 6293063ffa..12173ad13d 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -2,15 +2,17 @@ from . import ModuleStoreBase, Location from .exceptions import ItemNotFoundError +DRAFT = 'draft' + class DraftModuleStore(ModuleStoreBase): """ This mixin modifies a modulestore to give it draft semantics. - That is, edits made to units are stored to locations that have the revision 'draft', - and when reads are made, they first read with revision 'draft', and then fall back - to the baseline revision only if 'draft' doesn't exist. + That is, edits made to units are stored to locations that have the revision DRAFT, + and when reads are made, they first read with revision DRAFT, and then fall back + to the baseline revision only if DRAFT doesn't exist. - This module also includes functionality to promote 'draft' modules (and optionally + This module also includes functionality to promote DRAFT modules (and optionally their children) to published modules. """ @@ -34,7 +36,7 @@ class DraftModuleStore(ModuleStoreBase): get_children() to cache. None indicates to cache all descendents """ try: - return super(DraftModuleStore, self).get_item(Location(location)._replace(revision='draft'), depth) + return super(DraftModuleStore, self).get_item(Location(location)._replace(revision=DRAFT), depth) except ItemNotFoundError: return super(DraftModuleStore, self).get_item(location, depth) @@ -44,7 +46,7 @@ class DraftModuleStore(ModuleStoreBase): TODO (vshnayder): this may want to live outside the modulestore eventually """ try: - return super(DraftModuleStore, self).get_instance(course_id, Location(location)._replace(revision='draft')) + return super(DraftModuleStore, self).get_instance(course_id, Location(location)._replace(revision=DRAFT)) except ItemNotFoundError: return super(DraftModuleStore, self).get_instance(course_id, location) @@ -61,7 +63,7 @@ class DraftModuleStore(ModuleStoreBase): in the request. The depth is counted in the number of calls to get_children() to cache. None indicates to cache all descendents """ - draft_loc = Location(location)._replace(revision='draft') + draft_loc = Location(location)._replace(revision=DRAFT) draft_items = super(DraftModuleStore, self).get_items(draft_loc, depth) items = super(DraftModuleStore, self).get_items(location, depth) @@ -69,7 +71,7 @@ class DraftModuleStore(ModuleStoreBase): non_draft_items = [ item for item in items - if (item.location.revision != 'draft' + if (item.location.revision != DRAFT and item.location._replace(revision=None) not in draft_locs_found) ] return draft_items + non_draft_items @@ -79,7 +81,7 @@ class DraftModuleStore(ModuleStoreBase): Clone a new item that is a copy of the item at the location `source` and writes it to `location` """ - return super(DraftModuleStore, self).clone_item(source, Location(location)._replace(revision='draft')) + return super(DraftModuleStore, self).clone_item(source, Location(location)._replace(revision=DRAFT)) def update_item(self, location, data): """ @@ -89,7 +91,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location data: A nested dictionary of problem data """ - return super(DraftModuleStore, self).update_item(Location(location)._replace(revision='draft'), data) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_item(draft_loc, data) def update_children(self, location, children): """ @@ -99,7 +106,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location children: A list of child item identifiers """ - return super(DraftModuleStore, self).update_children(Location(location)._replace(revision='draft'), children) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_children(draft_loc, children) def update_metadata(self, location, metadata): """ @@ -109,7 +121,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location metadata: A nested dictionary of module metadata """ - return super(DraftModuleStore, self).update_metadata(Location(location)._replace(revision='draft'), metadata) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_metadata(draft_loc, metadata) def delete_item(self, location): """ @@ -117,7 +134,7 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location """ - return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision='draft')) + return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT)) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed @@ -125,4 +142,4 @@ class DraftModuleStore(ModuleStoreBase): returns an iterable of things that can be passed to Location. ''' - return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision='draft')) + return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision=DRAFT)) From 1a8532d8ad595a4d8ce6f4a9950fcba621eb574c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 14:29:24 -0400 Subject: [PATCH 0105/1010] Make it possible to create, edit, and publish a draft --- cms/djangoapps/contentstore/utils.py | 22 +++++++++++ cms/djangoapps/contentstore/views.py | 39 +++++++++++++++++-- .../coffee/src/views/module_edit.coffee | 4 +- cms/static/coffee/src/views/unit.coffee | 35 +++++++++++++++-- cms/static/sass/_unit.scss | 33 ++++++++++++++++ cms/templates/unit.html | 21 ++++------ cms/urls.py | 2 + .../lib/xmodule/xmodule/modulestore/draft.py | 16 ++++---- lms/envs/cms/dev.py | 22 +++++++---- 9 files changed, 156 insertions(+), 38 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4000f011ba..f103c74a8d 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,6 +1,9 @@ from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore +from xmodule.modulestore.draft import DRAFT +from xmodule.modulestore.exceptions import ItemNotFoundError + def get_course_location_for_item(location): ''' @@ -45,3 +48,22 @@ def get_lms_link_for_item(item): return lms_link + +def compute_unit_state(unit): + """ + Returns whether this unit is 'draft', 'public', or 'private'. + + 'draft' content is in the process of being edited, but still has a previous + version visible in the LMS + 'public' content is locked and visible in the LMS + 'private' content is editabled and not visible in the LMS + """ + + if unit.location.revision == DRAFT: + try: + modulestore('direct').get_item(unit.location._replace(revision=None)) + return 'draft' + except ItemNotFoundError: + return 'private' + else: + return 'public' diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 190b463383..cf60de7937 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME -from .utils import get_course_location_for_item, get_lms_link_for_item +from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state from xmodule.templates import all_templates @@ -210,6 +210,8 @@ def edit_unit(request, location): containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) + unit_state = compute_unit_state(item) + return render_to_response('unit.html', { 'unit': item, 'components': components, @@ -217,7 +219,9 @@ def edit_unit(request, location): 'lms_link': lms_link, 'subsection': containing_subsection, 'section': containing_section, - 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), + 'unit_state': unit_state, + 'release_date': None, }) @@ -235,7 +239,6 @@ def preview_component(request, location): }) - def user_author_string(user): '''Get an author string for commits by this user. Format: first last . @@ -428,7 +431,7 @@ def delete_item(request): item = modulestore().get_item(item_location) _delete_item(item, delete_children) - + return HttpResponse() @@ -479,6 +482,34 @@ def save_item(request): return HttpResponse() +@login_required +@expect_json +def create_draft(request): + location = request.POST['id'] + + # check permissions for this user within this course + if not has_access(request.user, location): + raise PermissionDenied() + + # This clones the existing item location to a draft location (the draft is implicit, + # because modulestore is a Draft modulestore) + modulestore().clone_item(location, location) + + return HttpResponse() + +@login_required +@expect_json +def publish_draft(request): + location = request.POST['id'] + + # check permissions for this user within this course + if not has_access(request.user, location): + raise PermissionDenied() + + modulestore().publish(location) + + return HttpResponse() + @login_required @expect_json def clone_item(request): diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 2326756dc8..85c099ab9a 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View return _metadata - cloneTemplate: (template) -> + cloneTemplate: (parent, template) -> $.post("/clone_item", { - parent_location: @$el.parent().data('id') + parent_location: parent template: template }, (data) => @model.set(id: data.id) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 1180b17471..3953e5ab92 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -5,7 +5,10 @@ class CMS.Views.UnitEdit extends Backbone.View 'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent' 'click .new-component-button': 'showNewComponentForm' - 'click .unit-actions .save-button': 'save' + 'click #save-draft': 'saveDraft' + 'click #delete-draft': 'deleteDraft' + 'click #create-draft': 'createDraft' + 'click #publish-draft': 'publishDraft' initialize: => @$newComponentItem = @$('.new-component-item') @@ -15,7 +18,6 @@ class CMS.Views.UnitEdit extends Backbone.View @$('.components').sortable( handle: '.drag-handle' - update: (event, ui) => @saveOrder() ) @$('.component').each((idx, element) => @@ -30,6 +32,7 @@ class CMS.Views.UnitEdit extends Backbone.View @model.components = @components() + # New component creation showNewComponentForm: (event) => event.preventDefault() @$newComponentItem.addClass('adding') @@ -61,13 +64,16 @@ class CMS.Views.UnitEdit extends Backbone.View @$newComponentItem.before(editor.$el) - editor.cloneTemplate($(event.currentTarget).data('location')) + editor.cloneTemplate( + @$el.data('id'), + $(event.currentTarget).data('location') + ) @closeNewComponent(event) components: => @$('.component').map((idx, el) -> $(el).data('id')).get() - saveOrder: => + saveDraft: => @model.save( children: @components() ) @@ -81,3 +87,24 @@ class CMS.Views.UnitEdit extends Backbone.View @saveOrder() ) + deleteDraft: (event) -> + $.post('/delete_item', { + id: @$el.data('id') + delete_children: true + }, => + window.location.reload() + ) + + createDraft: (event) -> + $.post('/create_draft', { + id: @$el.data('id') + }, => + @$el.toggleClass('edit-state-public edit-state-draft') + ) + + publishDraft: (event) -> + $.post('/publish_draft', { + id: @$el.data('id') + }, => + @$el.toggleClass('edit-state-public edit-state-draft') + ) \ No newline at end of file diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 7ed7e3ee7e..a030168cff 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -394,3 +394,36 @@ } } } + +.edit-state-draft { + .visibility { + display: none; + } + + #create-draft { + display: none; + } +} + +.edit-state-public { + #save-draft, + #delete-draft, + #publish-draft, + .component-actions, + .new-component-item { + display: none; + } + + .drag-handle { + display: none !important; + } +} + +.edit-state-private { + #save-draft, + #delete-draft, + #publish-draft, + #create-draft, { + display: none; + } +} diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 5434c11845..581ec0bef3 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -14,12 +14,12 @@ <%block name="content"> -
        +

        -
          +
            % for id in components:
          1. % endfor @@ -64,14 +64,6 @@

            Unit Properties

            -
            - - Set a due date - -
            + This unit has been published. Click here to edit it. + This unit has already been published. Click here to release your changes to it
            -

            This unit is scheduled to be released to students on 10/12/2012 with the subsection "Administrivia and Circuit Elements."

            +

            This unit is scheduled to be released to students on ${release_date} with the subsection ""

            @@ -114,4 +109,4 @@
        - \ No newline at end of file + diff --git a/cms/urls.py b/cms/urls.py index 890e9e2671..fa5377c277 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -15,6 +15,8 @@ urlpatterns = ('', url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'), url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'), + url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'), + url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.course_index', name='course_index'), url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 12173ad13d..2b94f892a4 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -136,10 +136,12 @@ class DraftModuleStore(ModuleStoreBase): """ return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT)) - def get_parent_locations(self, location): - '''Find all locations that are the parents of this location. Needed - for path_to_location(). - - returns an iterable of things that can be passed to Location. - ''' - return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision=DRAFT)) + def publish(self, location): + """ + Save a current draft to the underlying modulestore + """ + draft = self.get_item(location) + super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {})) + super(DraftModuleStore, self).update_children(location, draft.definition.get('children', [])) + super(DraftModuleStore, self).update_metadata(location, draft.metadata) + self.delete_item(location) diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py index 6e4697cccb..b192f12c93 100644 --- a/lms/envs/cms/dev.py +++ b/lms/envs/cms/dev.py @@ -4,16 +4,22 @@ Settings for the LMS that runs alongside the CMS on AWS from ..dev import * +modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'xmodule', + 'collection': 'modulestore', + 'fs_root': DATA_DIR, + 'render_template': 'mitxmako.shortcuts.render_to_string', +} + MODULESTORE = { 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'xmodule', - 'collection': 'modulestore', - 'fs_root': DATA_DIR, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } + 'OPTIONS': modulestore_options } } From 1328fc5ac0ceeb124e27d36afbe26e0760b07a46 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 15:27:02 -0400 Subject: [PATCH 0106/1010] Store published date in module metadata, and display it on draft pages --- cms/djangoapps/contentstore/utils.py | 6 ++-- cms/djangoapps/contentstore/views.py | 28 +++++++++++++------ cms/static/sass/_unit.scss | 6 ++-- cms/templates/unit.html | 16 ++++++++--- .../lib/xmodule/xmodule/modulestore/draft.py | 6 +++- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index f103c74a8d..687df11f62 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -35,13 +35,13 @@ def get_course_location_for_item(location): return location -def get_lms_link_for_item(item): +def get_lms_link_for_item(location): if settings.LMS_BASE is not None: lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_base=settings.LMS_BASE, # TODO: These will need to be changed to point to the particular instance of this problem in the particular course - course_id = modulestore().get_containing_courses(item.location)[0].id, - location=item.location, + course_id = modulestore().get_containing_courses(location)[0].id, + location=location, ) else: lms_link = None diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index cf60de7937..43ec799a66 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,11 +1,12 @@ from util.json_request import expect_json -import json -import os -import logging -import sys -import mimetypes -import StringIO import exceptions +import json +import logging +import mimetypes +import os +import StringIO +import sys +import time from collections import defaultdict from uuid import uuid4 @@ -154,7 +155,7 @@ def edit_subsection(request, location): item = modulestore().get_item(location) - lms_link = get_lms_link_for_item(item) + lms_link = get_lms_link_for_item(location) # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': @@ -183,7 +184,8 @@ def edit_unit(request, location): item = modulestore().get_item(location) - lms_link = get_lms_link_for_item(item) + # The non-draft location + lms_link = get_lms_link_for_item(item.location._replace(revision=None)) component_templates = defaultdict(list) @@ -212,16 +214,24 @@ def edit_unit(request, location): unit_state = compute_unit_state(item) + try: + published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date')) + except TypeError: + published_date = None + return render_to_response('unit.html', { 'unit': item, + 'unit_location': published_location, 'components': components, 'component_templates': component_templates, - 'lms_link': lms_link, + 'draft_preview_link': lms_link, + 'published_preview_link': lms_link, 'subsection': containing_subsection, 'section': containing_section, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'unit_state': unit_state, 'release_date': None, + 'published_date': published_date, }) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index a030168cff..fcd2f9a64d 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -87,7 +87,7 @@ } } - .rendered-component { + .xmodule_display { padding: 40px 20px 20px; } @@ -410,7 +410,8 @@ #delete-draft, #publish-draft, .component-actions, - .new-component-item { + .new-component-item, + #published-alert { display: none; } @@ -423,6 +424,7 @@ #save-draft, #delete-draft, #publish-draft, + #published-alert, #create-draft, { display: none; } diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 581ec0bef3..7f300d717c 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -8,14 +8,22 @@ new CMS.Views.UnitEdit({ el: $('.main-wrapper'), model: new CMS.Models.Module({ - id: '${unit.location.url()}' + id: '${unit_location}' }) }); <%block name="content"> -
        +
        +
        +

        You are editing a draft. + % if published_date: + This unit was originally published on ${published_date}. + % endif +

        + Preview the published version +

        @@ -60,7 +68,7 @@
        -
    From 5f66be503c5285727088976a179a15d7b52ccf58 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 11 Oct 2012 14:52:44 -0400 Subject: [PATCH 0196/1010] changed edx to edx studio in email templates (hopefully this is right) --- cms/templates/emails/activation_email.txt | 2 +- cms/templates/emails/activation_email_subject.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/emails/activation_email.txt b/cms/templates/emails/activation_email.txt index 209ff98335..6007f1e67b 100644 --- a/cms/templates/emails/activation_email.txt +++ b/cms/templates/emails/activation_email.txt @@ -1,4 +1,4 @@ -Thank you for signing up for edX! To activate your account, +Thank you for signing up for edX studio! To activate your account, please copy and paste this address into your web browser's address bar: diff --git a/cms/templates/emails/activation_email_subject.txt b/cms/templates/emails/activation_email_subject.txt index 495e0b5fad..07e5ea74db 100644 --- a/cms/templates/emails/activation_email_subject.txt +++ b/cms/templates/emails/activation_email_subject.txt @@ -1 +1 @@ -Your account for edX +Your account for edX studio From 82abdd07dc4600f248fb2746d93dc8b19a966d53 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Oct 2012 16:23:28 -0400 Subject: [PATCH 0197/1010] respond to some of Cale's comments --- .../xmodule/modulestore/xml_importer.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 1ab283e7f6..7adbd0aaa6 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -27,9 +27,8 @@ def import_static_content(modules, data_dir, static_content_store): if course_data_dir is None or course_loc is None: return remap_dict - ''' - now import all static assets - ''' + + # now import all static assets static_dir = '{0}/{1}/static/'.format(data_dir, course_data_dir) for dirname, dirnames, filenames in os.walk(static_dir): @@ -85,8 +84,10 @@ def import_from_xml(store, data_dir, course_dirs=None, load_error_modules=load_error_modules, ) + # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means + # to enumerate the entire collection of course modules. It will be left as a TBD to implement that + # method on XmlModuleStore. for course_id in module_store.modules.keys(): - remap_dict = {} if static_content_store is not None: remap_dict = import_static_content(module_store.modules[course_id], data_dir, static_content_store) @@ -103,10 +104,15 @@ def import_from_xml(store, data_dir, course_dirs=None, # cdodge: update any references to the static content paths # This is a bit brute force - simple search/replace - but it's unlikely that such references to '/static/....' # would occur naturally (in the wild) - if '/static/' in module_data: - for subkey in remap_dict.keys(): - module_data = module_data.replace('/static/' + subkey, 'xasset:' + remap_dict[subkey]) - + # @TODO, sorry a bit of technical debt here. There are some helper methods in xmodule_modifiers.py and static_replace.py which could + # better do the url replace on the html rendering side rather than on the ingest side + try: + if '/static/' in module_data: + for subkey in remap_dict.keys(): + module_data = module_data.replace('/static/' + subkey, 'xasset:' + remap_dict[subkey]) + except: + pass # part of the techincal debt is that module_data might not be a string (e.g. ABTest) + store.update_item(module.location, module_data) From 648792b7d329b60575d8f70c0db06b2f2a061808 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 11 Oct 2012 16:25:39 -0400 Subject: [PATCH 0198/1010] changed studio to edge in emails --- cms/templates/emails/activation_email.txt | 2 +- cms/templates/emails/activation_email_subject.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/emails/activation_email.txt b/cms/templates/emails/activation_email.txt index 6007f1e67b..5a1d63b670 100644 --- a/cms/templates/emails/activation_email.txt +++ b/cms/templates/emails/activation_email.txt @@ -1,4 +1,4 @@ -Thank you for signing up for edX studio! To activate your account, +Thank you for signing up for edX edge! To activate your account, please copy and paste this address into your web browser's address bar: diff --git a/cms/templates/emails/activation_email_subject.txt b/cms/templates/emails/activation_email_subject.txt index 07e5ea74db..0b0fb2ffe9 100644 --- a/cms/templates/emails/activation_email_subject.txt +++ b/cms/templates/emails/activation_email_subject.txt @@ -1 +1 @@ -Your account for edX studio +Your account for edX edge From 466e4cbd176b2f5171addc7601480ce72abe6a50 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Oct 2012 20:48:45 -0400 Subject: [PATCH 0199/1010] Fix regression on creating new sections and subsections. They were being set into 'draft' when we are not currently supporting draft modes for those areas of the hierarchy --- cms/djangoapps/contentstore/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 49bd1c7304..60264638a4 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -570,6 +570,7 @@ def unpublish_unit(request): return HttpResponse() + @login_required @expect_json def clone_item(request): @@ -581,10 +582,13 @@ def clone_item(request): if not has_access(request.user, parent_location): raise PermissionDenied() - parent = modulestore().get_item(parent_location) + # if we are creating a new section or subsection, then we don't want draft awareness + _modulestore = modulestore() if template.category not in ('sequential','chapter') else modulestore('direct') + + parent = _modulestore.get_item(parent_location) dest_location = parent_location._replace(category=template.category, name=uuid4().hex) - new_item = modulestore().clone_item(template, dest_location) + new_item = _modulestore.clone_item(template, dest_location) # TODO: This needs to be deleted when we have proper storage for static content new_item.metadata['data_dir'] = parent.metadata['data_dir'] @@ -593,7 +597,7 @@ def clone_item(request): if display_name is not None: new_item.metadata['display_name'] = display_name - modulestore().update_metadata(new_item.location.url(), new_item.own_metadata) + _modulestore.update_metadata(new_item.location.url(), new_item.own_metadata) if parent_location.category not in ('vertical',): parent_update_modulestore = modulestore('direct') From e27dea0fec54449eb42ae34ad4762ea24e0d256a Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Oct 2012 20:52:28 -0400 Subject: [PATCH 0200/1010] fix up dev.py to include the new flag --- cms/envs/dev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index e00c52a13e..faf7dc8886 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -12,6 +12,7 @@ TEMPLATE_DEBUG = DEBUG LOGGING = get_logger_config(ENV_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", + dev_env = True, debug=True) modulestore_options = { From c41c084b393b89cb88d7e72c80948d8cf5b31f1e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Oct 2012 09:55:32 -0400 Subject: [PATCH 0201/1010] Wire up login and register forms for edx landing page --- lms/static/sass/multicourse/_edge.scss | 4 +++ lms/templates/university_profile/edge.html | 32 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lms/static/sass/multicourse/_edge.scss b/lms/static/sass/multicourse/_edge.scss index 32580b964b..a099dded05 100644 --- a/lms/static/sass/multicourse/_edge.scss +++ b/lms/static/sass/multicourse/_edge.scss @@ -174,4 +174,8 @@ $paleYellow: #fffcf1; text-indent: -9999px; overflow: hidden; } + + #register .login-extra { + display: none; + } } \ No newline at end of file diff --git a/lms/templates/university_profile/edge.html b/lms/templates/university_profile/edge.html index e102555975..a6c714318c 100644 --- a/lms/templates/university_profile/edge.html +++ b/lms/templates/university_profile/edge.html @@ -1,15 +1,15 @@ <%inherit file="../stripped-main.html" /> +<%! from django.core.urlresolvers import reverse %> <%block name="title">edX edge <%block name="bodyclass">no-header edge-landing <%block name="content"> -
    edX edge
    - \ No newline at end of file + + +<%block name="js_extra"> + + + +<%include file="../signup_modal.html" /> \ No newline at end of file From 4814f8c38eac3cec167cba3619bd5d05eaba7e6c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Oct 2012 10:31:46 -0400 Subject: [PATCH 0202/1010] Cleaning up logos for edge landing page --- cms/static/sass/_landing.scss | 2 +- .../static/images}/edge-logo-large.png | Bin .../static/images}/edge-logo-small.png | Bin lms/static/images/edge-logo-large.png | Bin 7465 -> 0 bytes lms/static/images/edge-on-edx-logo.png | 1 + 5 files changed, 2 insertions(+), 1 deletion(-) rename {cms/static/img => common/static/images}/edge-logo-large.png (100%) rename {cms/static/img => common/static/images}/edge-logo-small.png (100%) delete mode 100644 lms/static/images/edge-logo-large.png create mode 120000 lms/static/images/edge-on-edx-logo.png diff --git a/cms/static/sass/_landing.scss b/cms/static/sass/_landing.scss index 9fbf00e1ec..16f1b5b5a7 100644 --- a/cms/static/sass/_landing.scss +++ b/cms/static/sass/_landing.scss @@ -119,7 +119,7 @@ width: 143px; height: 39px; margin: auto; - background: url(../img/edge-logo-small.png) no-repeat; + background: url(../images/edge-logo-small.png) no-repeat; text-indent: -9999px; overflow: hidden; } diff --git a/cms/static/img/edge-logo-large.png b/common/static/images/edge-logo-large.png similarity index 100% rename from cms/static/img/edge-logo-large.png rename to common/static/images/edge-logo-large.png diff --git a/cms/static/img/edge-logo-small.png b/common/static/images/edge-logo-small.png similarity index 100% rename from cms/static/img/edge-logo-small.png rename to common/static/images/edge-logo-small.png diff --git a/lms/static/images/edge-logo-large.png b/lms/static/images/edge-logo-large.png deleted file mode 100644 index 2933ec53493222881fd85a317c1e5f1e81f035e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7465 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 SRqUUimpU5y>MvC7BK`+2E0-Do diff --git a/lms/static/images/edge-on-edx-logo.png b/lms/static/images/edge-on-edx-logo.png new file mode 120000 index 0000000000..af5121d3f4 --- /dev/null +++ b/lms/static/images/edge-on-edx-logo.png @@ -0,0 +1 @@ +../../../common/static/images/edge-logo-small.png \ No newline at end of file From 14a1a7da314e39dafca8fbf5b84de3b4963ba24c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Oct 2012 10:33:31 -0400 Subject: [PATCH 0203/1010] allow importing of .tar.gz packages of courseware --- cms/djangoapps/auth/authz.py | 2 +- cms/djangoapps/contentstore/views.py | 48 ++++++++++++++++++- cms/static/sass/base-style.scss | 1 - cms/templates/index.html | 3 ++ cms/templates/widgets/import-course.html | 45 +++++++++++++++++ cms/urls.py | 2 + .../xmodule/modulestore/xml_importer.py | 7 ++- 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 cms/templates/widgets/import-course.html diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 659588f6f6..460c9d6467 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -46,7 +46,7 @@ def create_all_course_groups(creator, location): def create_new_course_group(creator, location, role): groupname = get_course_groupname_for_role(location, role) - (group, created) =Group.get_or_create(name=groupname) + (group, created) =Group.objects.get_or_create(name=groupname) if created: group.save() diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 77dfe8c77b..8911150fbb 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -8,6 +8,8 @@ import os import StringIO import sys import time +import tarfile +import shutil from collections import defaultdict from uuid import uuid4 @@ -44,10 +46,11 @@ from xmodule.contentstore.content import StaticContent from cache_toolbox.core import set_cached_content, get_cached_content, del_cached_content from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group -from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME +from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display from xmodule.templates import all_templates +from xmodule.modulestore.xml_importer import import_from_xml log = logging.getLogger(__name__) @@ -809,3 +812,46 @@ def asset_index(request, org, course, name): # points to the temporary edge page def edge(request): return render_to_response('university_profiles/edge.html', {}) + +def import_course(request): + if request.method != 'POST': + # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'? + return HttpResponseBadRequest() + + filename = request.FILES['file'].name + + if not filename.endswith('.tar.gz'): + return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'})) + + temp_filepath = settings.GITHUB_REPO_ROOT + '/' + filename + + logging.debug('importing course to {0}'.format(temp_filepath)) + + # stream out the uploaded files in chunks to disk + temp_file = open(temp_filepath, 'wb+') + for chunk in request.FILES['file'].chunks(): + temp_file.write(chunk) + temp_file.close() + + tf = tarfile.open(temp_filepath) + tf.extractall(settings.GITHUB_REPO_ROOT + '/') + + os.remove(temp_filepath) # remove the .tar.gz file + + # @todo: don't assume the top-level directory that was unziped was the same name (but without .tar.gz) + + course_dir = filename.replace('.tar.gz','') + + module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, + [course_dir], load_error_modules=False,static_content_store=contentstore()) + + # remove content directory - we *shouldn't* need this any longer :-) + shutil.rmtree(temp_filepath.replace('.tar.gz', '')) + + logging.debug('new course at {0}'.format(course_items[0].location)) + + create_all_course_groups(request.user, course_items[0].location) + + + + return HttpResponse(json.dumps({'Status' : 'OK'})) diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index ec5ece2338..f2256f97ed 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -17,7 +17,6 @@ @import "static-pages"; @import "users"; @import "course-info"; -@import "edge"; @import "landing"; @import "graphics"; @import "modal"; diff --git a/cms/templates/index.html b/cms/templates/index.html index 36ddd1044b..e63bbbb84d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -27,4 +27,7 @@
    + +<%include file="widgets/import-course.html"/> + diff --git a/cms/templates/widgets/import-course.html b/cms/templates/widgets/import-course.html new file mode 100644 index 0000000000..d3af4951d1 --- /dev/null +++ b/cms/templates/widgets/import-course.html @@ -0,0 +1,45 @@ +<%! from django.core.urlresolvers import reverse %> + +
    +
    + You can import an existing .tar.gz file of your course + + + + +
    +
    +
    0%
    +
    + +
    +
    + +
    + + + \ No newline at end of file diff --git a/cms/urls.py b/cms/urls.py index 47db1c024b..2fd4ed25e8 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -46,6 +46,8 @@ urlpatterns = ('', url(r'^edge$', 'contentstore.views.edge', name='edge'), url(r'^heartbeat$', include('heartbeat.urls')), + + url(r'import_course$', 'contentstore.views.import_course', name='import_course'), ) # User creation and updating views diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 7adbd0aaa6..37e57bbcd5 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -40,9 +40,6 @@ def import_static_content(modules, data_dir, static_content_store): content_loc = StaticContent.compute_location(course_loc.org, course_loc.course, fullname_with_subpath) mime_type = mimetypes.guess_type(filename)[0] - print 'importing static asset {0} of mime-type {1} from path {2}'.format(content_loc, - mime_type, content_path) - f = open(content_path, 'rb') data = f.read() f.close() @@ -87,6 +84,7 @@ def import_from_xml(store, data_dir, course_dirs=None, # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means # to enumerate the entire collection of course modules. It will be left as a TBD to implement that # method on XmlModuleStore. + course_items = [] for course_id in module_store.modules.keys(): remap_dict = {} if static_content_store is not None: @@ -97,6 +95,7 @@ def import_from_xml(store, data_dir, course_dirs=None, if module.category == 'course': # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this. module.metadata['hide_progress_tab'] = True + course_items.append(module) if 'data' in module.definition: module_data = module.definition['data'] @@ -124,4 +123,4 @@ def import_from_xml(store, data_dir, course_dirs=None, store.update_metadata(module.location, dict(module.own_metadata)) - return module_store + return module_store, course_items From 1015b3a974c354d8e2d63c3a0182b92a50304095 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Oct 2012 17:18:02 -0400 Subject: [PATCH 0204/1010] DraftMongoContentStore needs to both delete the draft *and* the non-draft entities --- common/lib/xmodule/xmodule/modulestore/draft.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 78600f1ba0..33ee9eaab2 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -157,7 +157,11 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location """ - return super(DraftModuleStore, self).delete_item(as_draft(location)) + + # cdodge: we should always delete both the draft and the original + super(DraftModuleStore, self).delete_item(as_draft(location)) + if (location.revision == None): # did caller request to delete the non-draft, if so be sure to do that + super(DraftModuleStore, self).delete_item(location) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed From 281f194b2a117b738e7db8b0cc66cd63ff3b987c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Oct 2012 09:25:13 -0400 Subject: [PATCH 0205/1010] per feedback, change back 'draft-aware' modulestore to not delete the published copy. In delete_item in views.py do the logic to determine whether we should be using a 'draft-aware' modulestore or not, based on the category type --- cms/djangoapps/contentstore/views.py | 10 ++++++++-- common/lib/xmodule/xmodule/modulestore/draft.py | 5 +---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 8911150fbb..7d27db51e2 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -470,10 +470,16 @@ def delete_item(request): item = modulestore().get_item(item_location) + _direct_delete_categories = ['course', 'chapter', 'sequential'] + + # @TODO: this probably leaves draft items dangling. + if delete_children: - _xmodule_recurse(item, lambda i: modulestore().delete_item(i.location)) + _xmodule_recurse(item, lambda i: modulestore('direct' if + i.location.category in _direct_delete_categories else 'direct').delete_item(i.location)) else: - modulestore().delete_item(item.location) + modulestore('direct' if + item.location.category in _direct_delete_categories else 'direct').delete_item(item.location) return HttpResponse() diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 33ee9eaab2..5fbf05ed9b 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -157,11 +157,8 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location """ + return super(DraftModuleStore, self).delete_item(as_draft(location)) - # cdodge: we should always delete both the draft and the original - super(DraftModuleStore, self).delete_item(as_draft(location)) - if (location.revision == None): # did caller request to delete the non-draft, if so be sure to do that - super(DraftModuleStore, self).delete_item(location) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed From 3c8de11f090bd6bc3fb655160935a1f078c8ce09 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Oct 2012 11:10:12 -0400 Subject: [PATCH 0206/1010] Standardize on how we bypass the draft-aware modulestore --- cms/djangoapps/contentstore/views.py | 37 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 7d27db51e2..85d0253a7e 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -57,6 +57,18 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] +DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential'] + + +def _modulestore(location): + """ + Returns the correct modulestore to use for modifying the specified location + """ + if location.category in DIRECT_ONLY_CATEGORIES: + return modulestore('direct') + else: + return modulestore() + # ==== Public views ================================================== @@ -470,16 +482,13 @@ def delete_item(request): item = modulestore().get_item(item_location) - _direct_delete_categories = ['course', 'chapter', 'sequential'] # @TODO: this probably leaves draft items dangling. if delete_children: - _xmodule_recurse(item, lambda i: modulestore('direct' if - i.location.category in _direct_delete_categories else 'direct').delete_item(i.location)) + _xmodule_recurse(item, lambda i: _modulestore(i.location).delete_item(i.location)) else: - modulestore('direct' if - item.location.category in _direct_delete_categories else 'direct').delete_item(item.location) + _modulestore(item.location).delete_item(item.location) return HttpResponse() @@ -591,28 +600,20 @@ def clone_item(request): if not has_access(request.user, parent_location): raise PermissionDenied() - # if we are creating a new section or subsection, then we don't want draft awareness - _modulestore = modulestore() if template.category not in ('sequential','chapter') else modulestore('direct') - - parent = _modulestore.get_item(parent_location) + parent = _modulestore(template).get_item(parent_location) dest_location = parent_location._replace(category=template.category, name=uuid4().hex) - new_item = _modulestore.clone_item(template, dest_location) + new_item = _modulestore(template).clone_item(template, dest_location) # TODO: This needs to be deleted when we have proper storage for static content new_item.metadata['data_dir'] = parent.metadata['data_dir'] - + # replace the display name with an optional parameter passed in from the caller if display_name is not None: new_item.metadata['display_name'] = display_name - _modulestore.update_metadata(new_item.location.url(), new_item.own_metadata) - - if parent_location.category not in ('vertical',): - parent_update_modulestore = modulestore('direct') - else: - parent_update_modulestore = modulestore() - parent_update_modulestore.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + _modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata) + _modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) return HttpResponse(json.dumps({'id': dest_location.url()})) From ac71da1535528ed9ab21a613397a13ed0c688d98 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 11 Oct 2012 14:18:02 -0400 Subject: [PATCH 0207/1010] Make header tabs work --- cms/djangoapps/contentstore/views.py | 27 +++++++++++++++++++++++++- cms/static/sass/_header.scss | 29 ++++++++++++++++++++++++---- cms/templates/base.html | 3 +-- cms/templates/manage_users.html | 1 - cms/templates/overview.html | 2 +- cms/templates/widgets/header.html | 17 ++++++++++------ 6 files changed, 64 insertions(+), 15 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 85d0253a7e..535862a784 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -157,6 +157,8 @@ def course_index(request, org, course, name): sections = course.get_children() return render_to_response('overview.html', { + 'active_tab': 'courseware', + 'context_course': course, 'sections': sections, 'parent_location': course.location, 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'), @@ -198,6 +200,7 @@ def edit_subsection(request, location): return render_to_response('edit_subsection.html', {'subsection': item, + 'context_course': course, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'lms_link': lms_link, 'parent_item' : parent, @@ -256,6 +259,7 @@ def edit_unit(request, location): published_date = None return render_to_response('unit.html', { + 'context_course': course, 'unit': item, 'unit_location': location, 'components': components, @@ -679,7 +683,11 @@ def manage_users(request, location): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): raise PermissionDenied() + course_module = modulestore().get_item(location) + return render_to_response('manage_users.html', { + 'active_tab': 'users', + 'context_course': course_module, 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME), 'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'), 'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/') @@ -755,7 +763,19 @@ def landing(request, org, course, coursename): def static_pages(request, org, course, coursename): - return render_to_response('static-pages.html', {}) + + location = ['i4x', org, course, 'course', coursename] + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + course = modulestore().get_item(location) + + return render_to_response('static-pages.html', { + 'active_tab': 'pages', + 'context_course': course, + }) def edit_static(request, org, course, coursename): @@ -784,11 +804,14 @@ def asset_index(request, org, course, name): if not has_access(request.user, location): raise PermissionDenied() + upload_asset_callback_url = reverse('upload_asset', kwargs = { 'org' : org, 'course' : course, 'coursename' : name }) + + course_module = modulestore().get_item(location) course_reference = StaticContent.compute_location(org, course, name) assets = contentstore().get_all_content_for_course(course_reference) @@ -811,6 +834,8 @@ def asset_index(request, org, course, name): asset_display.append(display_info) return render_to_response('asset_index.html', { + 'active_tab': 'assets', + 'context_course': course_module, 'assets': asset_display, 'upload_asset_callback_url': upload_asset_callback_url }) diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index d70f53b4df..be207c600f 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -4,6 +4,11 @@ body.no-header { } } +@mixin active { + @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3)); + @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset); +} + .primary-header { width: 100%; height: 36px; @@ -13,6 +18,26 @@ body.no-header { color: #fff; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset); + &.active-tab-courseware #courseware-tab { + @include active; + } + + &.active-tab-assets #assets-tab { + @include active; + } + + &.active-tab-pages #pages-tab { + @include active; + } + + &.active-tab-users #users-tab { + @include active; + } + + &.active-tab-import #import-tab { + @include active; + } + .left { width: 700px; } @@ -48,9 +73,5 @@ body.no-header { background: rgba(255, 255, 255, .1); } - &.active { - @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3)); - @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset); - } } } \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index f847ad6f7b..f839cb9753 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -18,8 +18,7 @@ - - <%include file="widgets/header.html"/> + <%include file="widgets/header.html" args="active_tab=active_tab"/> <%include file="courseware_vendor_js.html"/> diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index e479bc0942..142afc2304 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -1,7 +1,6 @@ <%inherit file="base.html" /> <%block name="title">Course Staff Manager <%block name="bodyclass">users -<%include file="widgets/header.html"/> <%block name="content">
    diff --git a/cms/templates/overview.html b/cms/templates/overview.html index d31e1e4823..e89d94b9c6 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -83,7 +83,7 @@
    - ${units.enum_units(subsection)} + ${units.enum_units(subsection)}
  6. % endfor
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index fb436ddde2..b46baebae3 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,16 +1,21 @@ <%! from django.core.urlresolvers import reverse %> -
+<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %> +
@@ -67,7 +67,7 @@ New Subsection
-
    +
      % for subsection in section.get_children():
    - -
    @@ -61,6 +62,8 @@ function hideNewUserForm(e) { e.preventDefault(); $newUserForm.slideUp(150); + $('#result').hide(); + $('#email').val(''); } $(document).ready(function() { @@ -78,7 +81,7 @@ data:JSON.stringify({ 'email': $('#email').val()}), }).done(function(data) { if (data.ErrMsg != undefined) - $('#result').empty().append(data.ErrMsg); + $('#result').show().empty().append(data.ErrMsg); else location.reload(); }) From f4822c23dee665b676b2219bd01f6fef4afabef0 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 11:52:31 -0400 Subject: [PATCH 0352/1010] lots of tweeks to better support importing of existing courseware --- cms/djangoapps/contentstore/views.py | 9 +- common/djangoapps/mitxmako/shortcuts.py | 2 +- common/djangoapps/static_replace.py | 24 +++- common/djangoapps/xmodule_modifiers.py | 4 +- common/lib/xmodule/setup.py | 2 + .../xmodule/xmodule/contentstore/content.py | 7 ++ common/lib/xmodule/xmodule/course_module.py | 3 +- .../lib/xmodule/xmodule/modulestore/mongo.py | 2 + common/lib/xmodule/xmodule/modulestore/xml.py | 52 ++++----- .../xmodule/modulestore/xml_importer.py | 103 ++++++++++++++---- common/lib/xmodule/xmodule/template_module.py | 20 +++- lms/djangoapps/courseware/courses.py | 26 +++-- lms/djangoapps/courseware/module_render.py | 3 +- lms/djangoapps/courseware/tests/tests.py | 11 +- lms/djangoapps/courseware/views.py | 2 +- 15 files changed, 186 insertions(+), 84 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index be4a157de4..4d1cfed553 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -287,9 +287,13 @@ def edit_unit(request, location): # TODO (cpennington): If we share units between courses, # this will need to change to check permissions correctly so as # to pick the correct parent subsection + + logging.debug('looking for parent of {0}'.format(location)) + containing_subsection_locs = modulestore().get_parent_locations(location) containing_subsection = modulestore().get_item(containing_subsection_locs[0]) + logging.debug('looking for parent of {0}'.format(containing_subsection.location)) containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) @@ -997,7 +1001,8 @@ def import_course(request, org, course, name): data_root = path(settings.GITHUB_REPO_ROOT) - course_dir = data_root / "{0}-{1}-{2}".format(org, course, name) + course_subdir = "{0}-{1}-{2}".format(org, course, name) + course_dir = data_root / course_subdir if not course_dir.isdir(): os.mkdir(course_dir) @@ -1033,7 +1038,7 @@ def import_course(request, org, course, name): shutil.move(r/fname, course_dir) module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, - [course_dir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location)) + [course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location)) # we can blow this away when we're done importing. shutil.rmtree(course_dir) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index ba22f2db20..181d3befd5 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -42,7 +42,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary.update(context) # fetch and render template template = middleware.lookup[namespace].get_template(template_name) - return template.render(**context_dictionary) + return template.render_unicode(**context_dictionary) def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): diff --git a/common/djangoapps/static_replace.py b/common/djangoapps/static_replace.py index 58e2c8da15..ee63e3cf93 100644 --- a/common/djangoapps/static_replace.py +++ b/common/djangoapps/static_replace.py @@ -5,6 +5,10 @@ from staticfiles.storage import staticfiles_storage from staticfiles import finders from django.conf import settings +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.xml import XMLModuleStore +from xmodule.contentstore.content import StaticContent + log = logging.getLogger(__name__) def try_staticfiles_lookup(path): @@ -22,7 +26,7 @@ def try_staticfiles_lookup(path): return url -def replace(static_url, prefix=None): +def replace(static_url, prefix=None, course_namespace=None): if prefix is None: prefix = '' else: @@ -41,13 +45,23 @@ def replace(static_url, prefix=None): return static_url.group(0) else: # don't error if file can't be found - url = try_staticfiles_lookup(prefix + static_url.group('rest')) - return "".join([quote, url, quote]) + # cdodge: to support the change over to Mongo backed content stores, lets + # use the utility functions in StaticContent.py + if static_url.group('prefix') == '/static/' and not isinstance(modulestore(), XMLModuleStore): + if course_namespace is None: + raise BaseException('You must pass in course_namespace when remapping static content urls with MongoDB stores') + url = StaticContent.convert_legacy_static_url(static_url.group('rest'), course_namespace) + else: + url = try_staticfiles_lookup(prefix + static_url.group('rest')) + + new_link = "".join([quote, url, quote]) + return new_link -def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/'): + +def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None): def replace_url(static_url): - return replace(static_url, staticfiles_prefix) + return replace(static_url, staticfiles_prefix, course_namespace = course_namespace) return re.sub(r""" (?x) # flags=re.VERBOSE diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 9473dfe26b..cda3d013cd 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -50,7 +50,7 @@ def replace_course_urls(get_html, course_id): return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') return _get_html -def replace_static_urls(get_html, prefix): +def replace_static_urls(get_html, prefix, course_namespace=None): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... @@ -59,7 +59,7 @@ def replace_static_urls(get_html, prefix): @wraps(get_html) def _get_html(): - return replace_urls(get_html(), staticfiles_prefix=prefix) + return replace_urls(get_html(), staticfiles_prefix=prefix, course_namespace = course_namespace) return _get_html diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index e9e78b5762..ff5be4d6df 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -37,6 +37,8 @@ setup( "discussion = xmodule.discussion_module:DiscussionDescriptor", "course_info = xmodule.html_module:HtmlDescriptor", "static_tab = xmodule.html_module:HtmlDescriptor", + "custom_tag_template = xmodule.raw_module:RawDescriptor", + "about = xmodule.html_module:HtmlDescriptor" ] } ) diff --git a/common/lib/xmodule/xmodule/contentstore/content.py b/common/lib/xmodule/xmodule/contentstore/content.py index 6dcf70fbe1..a7a76fa242 100644 --- a/common/lib/xmodule/xmodule/contentstore/content.py +++ b/common/lib/xmodule/xmodule/contentstore/content.py @@ -62,6 +62,13 @@ class StaticContent(object): @staticmethod def get_id_from_path(path): return get_id_from_location(get_location_from_path(path)) + + @staticmethod + def convert_legacy_static_url(path, course_namespace): + loc = StaticContent.compute_location(course_namespace.org, course_namespace.course, path) + return StaticContent.get_url_path_from_location(loc) + + class ContentStore(object): diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index ae3c01f639..94ab02cf39 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -202,7 +202,7 @@ class CourseDescriptor(SequenceDescriptor): # Try to load grading policy paths = ['grading_policy.json'] if policy_dir: - paths = [policy_dir + 'grading_policy.json'] + paths + paths = [policy_dir + '/grading_policy.json'] + paths policy = json.loads(cls.read_grading_policy(paths, system)) @@ -394,3 +394,4 @@ class CourseDescriptor(SequenceDescriptor): return self.location.org + diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 164bfd3590..deef3af3bf 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -1,5 +1,6 @@ import pymongo import sys +import logging from bson.son import SON from fs.osfs import OSFS @@ -49,6 +50,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): self.load_item, resources_fs, error_tracker, render_template) self.modulestore = modulestore self.module_data = module_data + self.default_class = default_class def load_item(self, location): diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index ddb437afe2..6794703998 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -427,42 +427,38 @@ class XMLModuleStore(ModuleStoreBase): # now import all pieces of course_info which is expected to be stored # in /info or /info/ - if url_name: - info_path = self.data_dir / course_dir / 'info' / url_name - - if not os.path.exists(info_path): - info_path = self.data_dir / course_dir / 'info' - - # we have a fixed number of .html info files that we expect there - for info_filename in ['handouts', 'guest_handouts', 'updates', 'guest_updates']: - filepath = info_path / info_filename + '.html' - if os.path.exists(filepath): - with open(filepath) as info_file: - html = info_file.read().decode('utf-8') - loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, 'course_info', info_filename) - info_module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc}) - self.modules[course_id][info_module.location] = info_module + self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name) # now import all static tabs which are expected to be stored in - # in /tabs or /tabs/ - if url_name: - tabs_path = self.data_dir / course_dir / 'tabs' / url_name + # in /tabs or /tabs/ + self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name) - if not os.path.exists(tabs_path): - tabs_path = self.data_dir / course_dir / 'tabs' + self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name) - for tab_filepath in glob.glob(tabs_path / '*.html'): - with open(tab_filepath) as tab_file: - html = tab_file.read().decode('utf-8') - # tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix - slug = os.path.splitext(os.path.basename(tab_filepath))[0] - loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, 'static_tab', slug) - tab_module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc}) - self.modules[course_id][tab_module.location] = tab_module + self.load_extra_content(system, course_descriptor, 'about', self.data_dir / course_dir / 'about', course_dir, url_name) log.debug('========> Done with course import from {0}'.format(course_dir)) return course_descriptor + def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name): + if url_name: + path = base_dir / url_name + + if not os.path.exists(path): + path = base_dir + + for filepath in glob.glob(path/ '*'): + with open(filepath) as f: + try: + html = f.read().decode('utf-8') + # tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix + slug = os.path.splitext(os.path.basename(filepath))[0] + loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug) + module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc}) + module.metadata['data_dir'] = course_dir + self.modules[course_descriptor.id][module.location] = module + except Exception, e: + logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e))) def get_instance(self, course_id, location, depth=0): """ diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index bced371001..6a7d44489b 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -1,6 +1,7 @@ import logging import os import mimetypes +from lxml.html import rewrite_links as lxml_rewrite_links from .xml import XMLModuleStore from .exceptions import DuplicateItemError @@ -9,7 +10,7 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX log = logging.getLogger(__name__) -def import_static_content(modules, data_dir, static_content_store): +def import_static_content(modules, data_dir, static_content_store, target_location_namespace): remap_dict = {} @@ -31,15 +32,13 @@ def import_static_content(modules, data_dir, static_content_store): # now import all static assets static_dir = '{0}/static/'.format(course_data_dir) - logging.debug("Importing static assets in {0}".format(static_dir)) - for dirname, dirnames, filenames in os.walk(static_dir): for filename in filenames: try: content_path = os.path.join(dirname, filename) fullname_with_subpath = content_path.replace(static_dir, '') # strip away leading path from the name - content_loc = StaticContent.compute_location(course_loc.org, course_loc.course, fullname_with_subpath) + content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath) mime_type = mimetypes.guess_type(filename)[0] f = open(content_path, 'rb') @@ -59,12 +58,50 @@ def import_static_content(modules, data_dir, static_content_store): #store the remapping information which will be needed to subsitute in the module data remap_dict[fullname_with_subpath] = content_loc.name - except: - raise + raise return remap_dict +def verify_content_links(module, base_dir, static_content_store, link, remap_dict = None): + if link.startswith('/static/'): + # yes, then parse out the name + path = link[len('/static/'):] + + static_pathname = base_dir / path + + if os.path.exists(static_pathname): + try: + content_loc = StaticContent.compute_location(module.location.org, module.location.course, path) + filename = os.path.basename(path) + mime_type = mimetypes.guess_type(filename)[0] + + f = open(static_pathname, 'rb') + data = f.read() + f.close() + + content = StaticContent(content_loc, filename, mime_type, data) + + # first let's save a thumbnail so we can get back a thumbnail location + thumbnail_content = static_content_store.generate_thumbnail(content) + + if thumbnail_content is not None: + content.thumbnail_location = thumbnail_content.location + + #then commit the content + static_content_store.save(content) + + new_link = StaticContent.get_url_path_from_location(content_loc) + + if remap_dict is not None: + remap_dict[link] = new_link + + return new_link + except Exception, e: + logging.exception('Skipping failed content load from {0}. Exception: {1}'.format(path, e)) + + return link + def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor', load_error_modules=True, static_content_store=None, target_location_namespace = None): @@ -94,15 +131,20 @@ def import_from_xml(store, data_dir, course_dirs=None, # method on XmlModuleStore. course_items = [] for course_id in module_store.modules.keys(): - remap_dict = {} + + course_data_dir = None + for module in module_store.modules[course_id].itervalues(): + if module.category == 'course': + course_data_dir = module.metadata['data_dir'] + if static_content_store is not None: - remap_dict = import_static_content(module_store.modules[course_id], data_dir, static_content_store) + import_static_content(module_store.modules[course_id], data_dir, static_content_store, + target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location) for module in module_store.modules[course_id].itervalues(): # remap module to the new namespace if target_location_namespace is not None: - # This looks a bit wonky as we need to also change the 'name' of the imported course to be what # the caller passed in if module.location.category != 'course': @@ -120,8 +162,8 @@ def import_from_xml(store, data_dir, course_dirs=None, child_loc = Location(child) new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) - - new_locs.append(new_child_loc) + + new_locs.append(new_child_loc.url()) module.definition['children'] = new_locs @@ -129,22 +171,37 @@ def import_from_xml(store, data_dir, course_dirs=None, if module.category == 'course': # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this. module.metadata['hide_progress_tab'] = True + + # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg + # so let's make sure we import in case there are no other references to it in the modules + verify_content_links(module, data_dir / course_data_dir, static_content_store, '/static/images/course_image.jpg') + course_items.append(module) if 'data' in module.definition: module_data = module.definition['data'] - # cdodge: update any references to the static content paths - # This is a bit brute force - simple search/replace - but it's unlikely that such references to '/static/....' - # would occur naturally (in the wild) - # @TODO, sorry a bit of technical debt here. There are some helper methods in xmodule_modifiers.py and static_replace.py which could - # better do the url replace on the html rendering side rather than on the ingest side - try: - if '/static/' in module_data: - for subkey in remap_dict.keys(): - module_data = module_data.replace('/static/' + subkey, 'xasset:' + remap_dict[subkey]) - except: - pass # part of the techincal debt is that module_data might not be a string (e.g. ABTest) + # cdodge: now go through any link references to '/static/' and make sure we've imported + # it as a StaticContent asset + try: + remap_dict = {} + + # use the rewrite_links as a utility means to enumerate through all links + # in the module data. We use that to load that reference into our asset store + # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to + # do the rewrites natively in that code. + # For example, what I'm seeing is -> + # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's + # no good, so we have to do this kludge + if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code + lxml_rewrite_links(module_data, lambda link: verify_content_links(module, data_dir / course_data_dir, + static_content_store, link, remap_dict)) + + for key in remap_dict.keys(): + module_data = module_data.replace(key, remap_dict[key]) + + except Exception, e: + logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) store.update_item(module.location, module_data) @@ -156,4 +213,6 @@ def import_from_xml(store, data_dir, course_dirs=None, # inherited metadata everywhere. store.update_metadata(module.location, dict(module.own_metadata)) + + return module_store, course_items diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py index 4df1c4ee3a..cca2cb0ca8 100644 --- a/common/lib/xmodule/xmodule/template_module.py +++ b/common/lib/xmodule/xmodule/template_module.py @@ -2,6 +2,7 @@ from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from lxml import etree from mako.template import Template +from xmodule.modulestore.django import modulestore class CustomTagModule(XModule): @@ -40,8 +41,7 @@ class CustomTagDescriptor(RawDescriptor): module_class = CustomTagModule template_dir_name = 'customtag' - @staticmethod - def render_template(system, xml_data): + def render_template(self, system, xml_data): '''Render the template, given the definition xml_data''' xmltree = etree.fromstring(xml_data) if 'impl' in xmltree.attrib: @@ -57,15 +57,23 @@ class CustomTagDescriptor(RawDescriptor): .format(location)) params = dict(xmltree.items()) - with system.resources_fs.open('custom_tags/{name}' - .format(name=template_name)) as template: - return Template(template.read()).render(**params) + + # cdodge: look up the template as a module + template_loc = self.location._replace(category='custom_tag_template', name=template_name) + + template_module = modulestore().get_item(template_loc) + template_module_data = template_module.definition['data'] + template = Template(template_module_data) + return template.render(**params) def __init__(self, system, definition, **kwargs): '''Render and save the template for this descriptor instance''' super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) - self.rendered_html = self.render_template(system, definition['data']) + + @property + def rendered_html(self): + return self.render_template(self.system, self.definition['data']) def export_to_file(self): """ diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 0103e9de00..c4dc6fa77b 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -14,6 +14,7 @@ from module_render import get_module from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore +from xmodule.contentstore.content import StaticContent from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.x_module import XModule @@ -68,8 +69,13 @@ def get_opt_course_with_access(user, course_id, action): def course_image_url(course): """Try to look up the image url for the course. If it's not found, log an error and return the dead link""" - path = course.metadata['data_dir'] + "/images/course_image.jpg" - return try_staticfiles_lookup(path) + if isinstance(modulestore(), XMLModuleStore): + path = course.metadata['data_dir'] + "/images/course_image.jpg" + return try_staticfiles_lookup(path) + else: + loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg') + path = StaticContent.get_url_path_from_location(loc) + return path def find_file(fs, dirs, filename): """ @@ -123,14 +129,12 @@ def get_course_about_section(course, section_key): 'effort', 'end_date', 'prerequisites', 'ocw_links']: try: - fs = course.system.resources_fs - # first look for a run-specific version - dirs = [path("about") / course.url_name, path("about")] - filepath = find_file(fs, dirs, section_key + ".html") - with fs.open(filepath) as htmlFile: - return replace_urls(htmlFile.read().decode('utf-8'), - course.metadata['data_dir']) - except ResourceNotFoundError: + loc = course.location._replace(category='about', name=section_key) + item = modulestore().get_instance(course.id, loc) + + return item.definition['data'] + + except ItemNotFoundError: log.warning("Missing about section {key} in course {url}".format( key=section_key, url=course.location.url())) return None @@ -160,8 +164,6 @@ def get_course_info_section(request, cache, course, section_key): loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) course_module = get_module(request.user, request, loc, cache, course.id) - logging.debug('course_module = {0}'.format(course_module)) - html = '' if course_module is not None: diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 3c9958b36d..d9f87d77b6 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -256,7 +256,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi module.get_html = replace_static_urls( wrap_xmodule(module.get_html, module, 'xmodule_display.html'), - module.metadata['data_dir'] if 'data_dir' in module.metadata else '') + module.metadata['data_dir'] if 'data_dir' in module.metadata else '', + course_namespace = module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 3de344b156..8bdcf59815 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -247,6 +247,7 @@ class PageLoader(ActivateLoginTestCase): all_ok = True for descriptor in modstore.get_items( Location(None, None, None, None, None)): + n += 1 print "Checking ", descriptor.location.url() #print descriptor.__class__, descriptor.location @@ -256,9 +257,13 @@ class PageLoader(ActivateLoginTestCase): msg = str(resp.status_code) if resp.status_code != 302: - msg = "ERROR " + msg - all_ok = False - num_bad += 1 + # cdodge: we're adding new module category as part of the Studio work + # such as 'course_info', etc, which are not supposed to be jump_to'able + # so a successful return value here should be a 404 + if descriptor.location.category not in ['about', 'static_tab', 'course_info', 'custom_tag_template'] or resp.status_code != 404: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 print msg self.assertTrue(all_ok) # fail fast diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index fc834768e7..bacc41fdf4 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -421,7 +421,7 @@ def course_about(request, course_id): settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION')) return render_to_response('portal/course_about.html', - {'course': course, + { 'course': course, 'registered': registered, 'course_target': course_target, 'show_courseware_link' : show_courseware_link}) From ceb17ad41163aa6739d1e6b89edea59b3e9e0dc2 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 30 Oct 2012 12:57:47 -0400 Subject: [PATCH 0353/1010] added handling for long class names in the header --- cms/static/sass/_header.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss index 5255819a60..e80c7aa3b1 100644 --- a/cms/static/sass/_header.scss +++ b/cms/static/sass/_header.scss @@ -43,7 +43,14 @@ body.no-header { } .left { - width: 700px; + width: 750px; + } + + .class-name { + max-width: 350px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .drop-icon { From d68945de334912ba24cfbd93948f0ecd572d2e60 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Tue, 30 Oct 2012 15:04:21 -0400 Subject: [PATCH 0354/1010] started adding settings page --- cms/djangoapps/contentstore/views.py | 4 +++ cms/static/sass/_settings.scss | 39 ++++++++++++++++++++++ cms/static/sass/base-style.scss | 1 + cms/templates/settings.html | 48 ++++++++++++++++++++++++++++ cms/urls.py | 1 + 5 files changed, 93 insertions(+) create mode 100644 cms/static/sass/_settings.scss create mode 100644 cms/templates/settings.html diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 05cde6043d..8794404118 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -869,6 +869,10 @@ def edit_static(request, org, course, coursename): return render_to_response('edit-static-page.html', {}) +def settings(request, org, course, coursename): + return render_to_response('settings.html', {}) + + def not_found(request): return render_to_response('error.html', {'error': '404'}) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss new file mode 100644 index 0000000000..7dd349adb9 --- /dev/null +++ b/cms/static/sass/_settings.scss @@ -0,0 +1,39 @@ +.settings { + .settings-overview { + @extend .window; + padding: 30px 40px; + + .details { + margin-bottom: 20px; + font-size: 14px; + } + + .row { + margin-bottom: 20px; + padding-bottom: 0; + border-bottom: none; + } + + label { + display: inline-block; + width: 200px; + font-size: 14px; + + &.check-label { + display: inline; + } + } + } + + h2 { + margin-bottom: 20px; + font-size: 24px; + font-weight: 300; + } + + section { + border-bottom: 1px solid $mediumGrey; + margin-bottom: 40px; + padding-bottom: 40px; + } +} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 6a6105c109..73812125a8 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -18,6 +18,7 @@ @import "static-pages"; @import "users"; @import "import"; +@import "settings"; @import "course-info"; @import "landing"; @import "graphics"; diff --git a/cms/templates/settings.html b/cms/templates/settings.html new file mode 100644 index 0000000000..8df3cede31 --- /dev/null +++ b/cms/templates/settings.html @@ -0,0 +1,48 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="bodyclass">settings +<%block name="title">Settings + +<%namespace name='static' file='static_content.html'/> + +<%block name="jsextra"> + + + +<%block name="content"> +
    +
    +

    Settings

    +
    +
    +

    Details

    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +

    Grading

    +
    + + +
    +
    +
    +

    Problems

    +
    + +
    +
    +
    +
    +
    + diff --git a/cms/urls.py b/cms/urls.py index 7b3dd90a0b..620460ad35 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -37,6 +37,7 @@ urlpatterns = ('', url(r'^pages/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.static_pages', name='static_pages'), url(r'^edit_static/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_static', name='edit_static'), url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$', 'contentstore.views.asset_index', name='asset_index'), + url(r'^settings/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.settings', name='settings'), # temporary landing page for a course url(r'^edge/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.landing', name='landing'), From 743f2b56dd1d3c1c80ce239be8539338ed1967d7 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 15:12:13 -0400 Subject: [PATCH 0355/1010] make course about view methods render the about content as a module, so we get all the url rewriting goodness. Also, since we're now handling the url re-writing via the module get_html pipelines, we can remove the link rewriting inside the xmodules itself - which is good because there's a wierd bug in lxml.html rewriting --- common/djangoapps/xmodule_modifiers.py | 1 + common/lib/xmodule/xmodule/capa_module.py | 12 ------- common/lib/xmodule/xmodule/html_module.py | 10 +----- .../xmodule/modulestore/xml_importer.py | 4 +-- common/lib/xmodule/xmodule/x_module.py | 18 ---------- lms/djangoapps/courseware/courses.py | 34 +++++++++++++++++-- lms/djangoapps/courseware/module_render.py | 2 ++ 7 files changed, 37 insertions(+), 44 deletions(-) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index cda3d013cd..431181bfac 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -59,6 +59,7 @@ def replace_static_urls(get_html, prefix, course_namespace=None): @wraps(get_html) def _get_html(): + logging.debug('in replace_static_urls') return replace_urls(get_html(), staticfiles_prefix=prefix, course_namespace = course_namespace) return _get_html diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d75e0ff860..587fc09eed 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -10,7 +10,6 @@ import sys from datetime import timedelta from lxml import etree -from lxml.html import rewrite_links from pkg_resources import resource_string from capa.capa_problem import LoncapaProblem @@ -342,17 +341,6 @@ class CapaModule(XModule): html = '
    '.format( id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
    " - # cdodge: OK, we have to do two rounds of url reference subsitutions - # one which uses the 'asset library' that is served by the contentstore and the - # more global /static/ filesystem based static content. - # NOTE: rewrite_content_links is defined in XModule - # This is a bit unfortunate and I'm sure we'll try to considate this into - # a one step process. - try: - html = rewrite_links(html, self.rewrite_content_links) - except: - logging.error('error rewriting links in {0}'.format(html)) - # now do the substitutions which are filesystem based, e.g. '/static/' prefixes return self.system.replace_urls(html, self.metadata['data_dir']) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 00577912c8..f6dddfdd4c 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -4,7 +4,6 @@ import logging import os import sys from lxml import etree -from lxml.html import rewrite_links from path import path from .x_module import XModule @@ -29,14 +28,7 @@ class HtmlModule(XModule): js_module_name = "HTMLModule" def get_html(self): - # cdodge: perform link substitutions for any references to course static content (e.g. images) - _html = self.html - try: - _html = rewrite_links(_html, self.rewrite_content_links) - except: - logging.error('error rewriting links on the following HTML content: {0}'.format(_html)) - - return _html + return self.html def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 6a7d44489b..3c94e25aa2 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -30,7 +30,7 @@ def import_static_content(modules, data_dir, static_content_store, target_locati # now import all static assets - static_dir = '{0}/static/'.format(course_data_dir) + static_dir = '{0}/static/'.format(data_dir / course_data_dir) for dirname, dirnames, filenames in os.walk(static_dir): for filename in filenames: @@ -185,7 +185,7 @@ def import_from_xml(store, data_dir, course_dirs=None, # it as a StaticContent asset try: remap_dict = {} - + # use the rewrite_links as a utility means to enumerate through all links # in the module data. We use that to load that reference into our asset store # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 99468946d7..2174d28112 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -320,24 +320,6 @@ class XModule(HTMLSnippet): get is a dictionary-like object ''' return "" - # cdodge: added to support dynamic substitutions of - # links for courseware assets (e.g. images). is passed through from lxml.html parser - def rewrite_content_links(self, link): - loc = Location(self.location) - return XModule._rewrite_content_links(loc, link) - - - @staticmethod - def _rewrite_content_links(loc, link): - if link.startswith(XASSET_SRCREF_PREFIX): - # yes, then parse out the name - name = link[len(XASSET_SRCREF_PREFIX):] - # resolve the reference to our internal 'filepath' which - content_loc = StaticContent.compute_location(loc.org, loc.course, name) - link = StaticContent.get_url_path_from_location(content_loc) - - return link - def policy_key(location): """ diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index c4dc6fa77b..4b020f3ead 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -2,6 +2,7 @@ from collections import defaultdict from fs.errors import ResourceNotFoundError from functools import wraps import logging +import inspect from lxml.html import rewrite_links @@ -21,11 +22,24 @@ from xmodule.x_module import XModule from static_replace import replace_urls, try_staticfiles_lookup from courseware.access import has_access import branding - - +from courseware.models import StudentModuleCache log = logging.getLogger(__name__) +def get_request_for_thread(): + """Walk up the stack, return the nearest first argument named "request".""" + frame = None + try: + for f in inspect.stack()[1:]: + frame = f[0] + code = frame.f_code + if code.co_varnames[:1] == ("request",): + return frame.f_locals["request"] + elif code.co_varnames[:2] == ("self", "request",): + return frame.f_locals["request"] + finally: + del frame + def get_course_by_id(course_id): """ @@ -129,10 +143,23 @@ def get_course_about_section(course, section_key): 'effort', 'end_date', 'prerequisites', 'ocw_links']: try: + + request = get_request_for_thread() + + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( + course.id, request.user, course, depth=2) + loc = course.location._replace(category='about', name=section_key) + course_module = get_module(request.user, request, loc, student_module_cache, course.id) + + html = '' + + if course_module is not None: + html = course_module.get_html() + item = modulestore().get_instance(course.id, loc) - return item.definition['data'] + return html except ItemNotFoundError: log.warning("Missing about section {key} in course {url}".format( @@ -161,6 +188,7 @@ def get_course_info_section(request, cache, course, section_key): - guest_updates """ + loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) course_module = get_module(request.user, request, loc, cache, course.id) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d9f87d77b6..0e3b3a326c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -259,6 +259,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi module.metadata['data_dir'] if 'data_dir' in module.metadata else '', course_namespace = module.location._replace(category=None, name=None)) + logging.debug('in get_module') + # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) From ee1098c03247aa6b7ba32283b95d81e4714320e3 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 15:37:40 -0400 Subject: [PATCH 0356/1010] remove unused line --- lms/djangoapps/courseware/courses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 4b020f3ead..ca84fb3c65 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -157,8 +157,6 @@ def get_course_about_section(course, section_key): if course_module is not None: html = course_module.get_html() - item = modulestore().get_instance(course.id, loc) - return html except ItemNotFoundError: From cd9fbaeb024262f7f15f76581de8746d945af483 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 30 Oct 2012 15:51:21 -0400 Subject: [PATCH 0357/1010] Add timeout so that jasmine tests eventually fail if the server doesn't start --- cms/envs/common.py | 1 + lms/envs/common.py | 1 + rakefile | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/cms/envs/common.py b/cms/envs/common.py index f110ede87a..4b4b69ad39 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -35,6 +35,7 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION_SERVICE': False, 'AUTH_USE_MIT_CERTIFICATES' : False, } +ENABLE_JASMINE = False # needed to use lms student app GENERATE_RANDOM_USER_CREDENTIALS = False diff --git a/lms/envs/common.py b/lms/envs/common.py index 251427d014..2af62182ac 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -30,6 +30,7 @@ from .discussionsettings import * ################################### FEATURES ################################### COURSEWARE_ENABLED = True +ENABLE_JASMINE = False GENERATE_RANDOM_USER_CREDENTIALS = False PERFSTATS = False diff --git a/rakefile b/rakefile index e0972df12a..5cdc5b3214 100644 --- a/rakefile +++ b/rakefile @@ -48,7 +48,11 @@ def django_for_jasmine(system) puts django_pid jasmine_url = 'http://localhost:12345/_jasmine/' up = false + start_time = Time.now until up do + if Time.now - start_time > 30 + abort "Timed out waiting for server to start to run jasmine tests" + end begin response = Net::HTTP.get_response(URI(jasmine_url)) puts response.code From ba9a03410b39017005c0e31c57641139f2f81546 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 15:56:02 -0400 Subject: [PATCH 0358/1010] remove unneeded debugging traces --- cms/djangoapps/contentstore/views.py | 3 --- common/lib/xmodule/xmodule/modulestore/mongo.py | 1 - lms/djangoapps/courseware/module_render.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 4d1cfed553..b3af6f7e7b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -288,12 +288,9 @@ def edit_unit(request, location): # this will need to change to check permissions correctly so as # to pick the correct parent subsection - logging.debug('looking for parent of {0}'.format(location)) - containing_subsection_locs = modulestore().get_parent_locations(location) containing_subsection = modulestore().get_item(containing_subsection_locs[0]) - logging.debug('looking for parent of {0}'.format(containing_subsection.location)) containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index deef3af3bf..550e6570ac 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -50,7 +50,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem): self.load_item, resources_fs, error_tracker, render_template) self.modulestore = modulestore self.module_data = module_data - self.default_class = default_class def load_item(self, location): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0e3b3a326c..d9f87d77b6 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -259,8 +259,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi module.metadata['data_dir'] if 'data_dir' in module.metadata else '', course_namespace = module.location._replace(category=None, name=None)) - logging.debug('in get_module') - # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) From 6221baf3e5755c1398215efc4fac6c684daf8910 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 30 Oct 2012 15:58:59 -0400 Subject: [PATCH 0359/1010] Use TEST_ROOT log directory when running jasmine tests --- cms/envs/jasmine.py | 2 +- lms/envs/jasmine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index b42744a520..f8ed6e0beb 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -8,7 +8,7 @@ from logsettings import get_logger_config ENABLE_JASMINE = True DEBUG = True -LOGGING = get_logger_config(ENV_ROOT / "log", +LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, diff --git a/lms/envs/jasmine.py b/lms/envs/jasmine.py index 43459f79aa..14c504e34f 100644 --- a/lms/envs/jasmine.py +++ b/lms/envs/jasmine.py @@ -8,7 +8,7 @@ from logsettings import get_logger_config ENABLE_JASMINE = True DEBUG = True -LOGGING = get_logger_config(ENV_ROOT / "log", +LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, From f0b4371c90d812ad62f686fa0dc788f4a2b1f28e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 16:01:28 -0400 Subject: [PATCH 0360/1010] addressed Cale's feedback --- lms/djangoapps/courseware/courses.py | 2 +- lms/djangoapps/courseware/tabs.py | 8 ++++---- lms/djangoapps/courseware/views.py | 7 +------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index ca84fb3c65..1d88c4e5a3 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -150,7 +150,7 @@ def get_course_about_section(course, section_key): course.id, request.user, course, depth=2) loc = course.location._replace(category='about', name=section_key) - course_module = get_module(request.user, request, loc, student_module_cache, course.id) + course_module = get_module(request.user, request, loc, None, course.id) html = '' diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 9f826b77f0..e926a0b114 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -260,13 +260,13 @@ def get_static_tab_by_slug(course, tab_slug): def get_static_tab_contents(request, cache, course, tab): loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug']) - course_module = get_module(request.user, request, loc, cache, course.id) + tab_module = get_module(request.user, request, loc, cache, course.id) - logging.debug('course_module = {0}'.format(course_module)) + logging.debug('course_module = {0}'.format(tab_module)) html = '' - if course_module is not None: - html = course_module.get_html() + if tab_module is not None: + html = tab_module.get_html() return html diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index bacc41fdf4..14bec50af3 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -347,12 +347,7 @@ def course_info(request, course_id): course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') - - - cache = StudentModuleCache.cache_for_descriptor_descendents( - course.id, request.user, course, depth=2) - - return render_to_response('courseware/info.html', {'request' : request, 'course_id' : course_id, 'cache' : cache, + return render_to_response('courseware/info.html', {'request' : request, 'course_id' : course_id, 'cache' : None, 'course': course, 'staff_access': staff_access}) @ensure_csrf_cookie From 34c442a1dcd40e4947b775d1de39d2f098bd50ce Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 30 Oct 2012 16:04:51 -0400 Subject: [PATCH 0361/1010] removed unneeded trace log. Also don't need StudentModuleCache in 'about' section rendering --- common/djangoapps/xmodule_modifiers.py | 1 - lms/djangoapps/courseware/courses.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 431181bfac..cda3d013cd 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -59,7 +59,6 @@ def replace_static_urls(get_html, prefix, course_namespace=None): @wraps(get_html) def _get_html(): - logging.debug('in replace_static_urls') return replace_urls(get_html(), staticfiles_prefix=prefix, course_namespace = course_namespace) return _get_html diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 1d88c4e5a3..aa3fbf12bb 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -146,9 +146,6 @@ def get_course_about_section(course, section_key): request = get_request_for_thread() - student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - course.id, request.user, course, depth=2) - loc = course.location._replace(category='about', name=section_key) course_module = get_module(request.user, request, loc, None, course.id) From 0674d4e74b0c5bc784b8f5663c9234e15984fec5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 30 Oct 2012 16:16:09 -0400 Subject: [PATCH 0362/1010] Only allow django-admin to run in reload mode when browsing jasmine tests, and only kill the whole process group in that case (so as to know kill jenkins) --- rakefile | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rakefile b/rakefile index 5cdc5b3214..e22b19df0c 100644 --- a/rakefile +++ b/rakefile @@ -41,9 +41,13 @@ def django_admin(system, env, command, *args) return "#{django_admin} #{command} --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}" end -def django_for_jasmine(system) +def django_for_jasmine(system, django_reload) + if !django_reload + reload_arg = '--noreload' + end + django_pid = fork do - exec(*django_admin(system, 'jasmine', 'runserver', '12345').split(' ')) + exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' ')) end puts django_pid jasmine_url = 'http://localhost:12345/_jasmine/' @@ -67,7 +71,11 @@ def django_for_jasmine(system) begin yield jasmine_url ensure - Process.kill(:SIGKILL, -Process.getpgid(django_pid)) + if django_reload + Process.kill(:SIGKILL, -Process.getpgid(django_pid)) + else + Process.kill(:SIGKILL, django_pid) + end Process.wait(django_pid) end end @@ -116,7 +124,7 @@ end desc "Open jasmine tests in your default browser" task "browse_jasmine_#{system}" do - django_for_jasmine(system) do |jasmine_url| + django_for_jasmine(system, true) do |jasmine_url| Launchy.open(jasmine_url) puts "Press ENTER to terminate".red $stdin.gets @@ -126,7 +134,7 @@ end desc "Use phantomjs to run jasmine tests from the console" task "phantomjs_jasmine_#{system}" do phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' - django_for_jasmine(system) do |jasmine_url| + django_for_jasmine(system, false) do |jasmine_url| sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}") end end From d4a6b589f119644aa3a2dadc73f55e1ad9089b64 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 30 Oct 2012 16:48:47 -0400 Subject: [PATCH 0363/1010] Add comment about the target of change log entries --- cms/CHANGELOG.md | 5 ++++- lms/CHANGELOG.md | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cms/CHANGELOG.md b/cms/CHANGELOG.md index 6c43dfb681..bdd325bd8d 100644 --- a/cms/CHANGELOG.md +++ b/cms/CHANGELOG.md @@ -2,11 +2,14 @@ Instructions ============ For each pull request, add one or more lines to the bottom of the change list. When code is released to production, change the `Upcoming` entry to todays date, and add -a new block at the bottom of the file +a new block at the bottom of the file. Upcoming -------- +Change log entries should be targeted at end users. A good place to start is the +user story that instigated the pull request. + Changes ======= diff --git a/lms/CHANGELOG.md b/lms/CHANGELOG.md index 6c43dfb681..0794d379b9 100644 --- a/lms/CHANGELOG.md +++ b/lms/CHANGELOG.md @@ -2,11 +2,13 @@ Instructions ============ For each pull request, add one or more lines to the bottom of the change list. When code is released to production, change the `Upcoming` entry to todays date, and add -a new block at the bottom of the file +a new block at the bottom of the file. Upcoming -------- +Change log entries should be targeted at end users. A good place to start is the +user story that instigated the pull request. Changes ======= From 8d4ee3b05ee632fa8a0b032f941ee8bf198f5120 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 31 Oct 2012 10:37:21 -0400 Subject: [PATCH 0364/1010] Add more documentation of development tasks --- doc/development.md | 78 +++++++++++++++++++++++++++++++++------------- rakefile | 14 +++++++++ 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/doc/development.md b/doc/development.md index b4ac52d202..c38cda56ff 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,35 +1,53 @@ -# Running the CMS +# Development Tasks -One can start the CMS by running `rake cms`. This will run the server on localhost -port 8001. +## Prerequisites -However, the server also needs data to work from. +### Ruby -## Installing Mongodb +To install all of the libraries needed for our rake commands, run `bundle install`. +This will read the `Gemfile` and install all of the gems specified there. -Please see http://www.mongodb.org/downloads for more detailed instructions. +### Python -### Ubuntu +In order, run the following: - sudo apt-get install mongodb + pip install -r pre-requirements.txt + pip install -r requirements.txt + pip install -r test-requirements.txt -### OSX +### Binaries -Use the MacPorts package `mongodb` or the Homebrew formula `mongodb` +Install the following: -## Initializing Mongodb +* Mongodb (http://www.mongodb.org/) -Check out the course data directories that you want to work with into the -`GITHUB_REPO_ROOT` (by default, `../data`). Then run the following command: +### Databases +Run the following to setup the relational database before starting servers: - rake django-admin[import,cms,dev,../data] + rake resetdb -Replace `../data` with your `GITHUB_REPO_ROOT` if it's not the default value. +## Starting development servers -This will import all courses in your data directory into mongodb +Both the LMS and Studio can be started using the following shortcut tasks -## Unit tests + rake lms # Start the LMS + rake cms # Start studio + +Under the hood, this executes `django-admin.py runserver --pythonpath=$WORKING_DIRECTORY --settings=lms.envs.dev`, +which starts a local development server. + +Both of these commands take arguments to start the servers in different environments +or with additional options: + + # Start the LMS using the test configuration, on port 5000 + rake lms[test,5000] # Executes django-admin.py runserver --pythonpath=$WORKING_DIRECTORY --setings=lms.envs.test 5000 + +*N.B.* You may have to escape the `[` characters, depending on your shell: `rake "lms[test,5000]"` + +## Running tests + +### Python Tests This runs all the tests (long, uses collectstatic): @@ -43,10 +61,6 @@ xmodule can be tested independently, with this: rake test_common/lib/xmodule -To see all available rake commands, do this: - - rake -T - To run a single django test class: django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth @@ -67,6 +81,28 @@ To run a single nose test: Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html +### Javascript Tests + +These commands start a development server with jasmine testing enabled, and launch your default browser +pointing to those tests + + rake browse_jasmine_{lms,cms} + +To run the tests headless, you must install phantomjs (http://phantomjs.org/download.html). + + rake phantomjs_jasmine_{lms,cms} + +If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it + + PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms} + + +## Getting More Information + +Run the following to see a list of all rake tasks available and their arguments + + rake -T + ## Content development If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore. diff --git a/rakefile b/rakefile index e22b19df0c..798898310e 100644 --- a/rakefile +++ b/rakefile @@ -197,6 +197,20 @@ TEST_TASKS = [] end end + +desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" +task :resetdb, [:env] do |t, args| + args.with_defaults(:env => 'dev') + sh(django_admin(:lms, args.env, 'syncdb')) + sh(django_admin(:lms, args.env, 'migrate')) +end + +desc "Update the relational database to the latest migration" +task :migrate, [:env] do |t, args| + args.with_defaults(:env => 'dev') + sh(django_admin(:lms, args.env, 'migrate')) +end + Dir["common/lib/*"].each do |lib| task_name = "test_#{lib}" From 2ca63268d6c7082c2e19868456750eda3f663db0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 31 Oct 2012 10:39:26 -0400 Subject: [PATCH 0365/1010] Add commands for Studio LMS --- doc/development.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/development.md b/doc/development.md index c38cda56ff..d68b49228e 100644 --- a/doc/development.md +++ b/doc/development.md @@ -33,6 +33,8 @@ Both the LMS and Studio can be started using the following shortcut tasks rake lms # Start the LMS rake cms # Start studio + rake lms[cms.dev] # Start LMS to run alongside Studio + rake lms[cms.dev_preview] # Start LMS to run alongside Studio in preview mode Under the hood, this executes `django-admin.py runserver --pythonpath=$WORKING_DIRECTORY --settings=lms.envs.dev`, which starts a local development server. From 255720abd7d059d772b3666ea9defc8da05516f3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 31 Oct 2012 13:21:36 -0400 Subject: [PATCH 0366/1010] Clean old javascript tests from CAS --- cms/envs/jasmine.py | 5 +- cms/static/coffee/spec/main_spec.coffee | 66 --------- .../coffee/spec/models/module_spec.coffee | 73 +--------- .../coffee/spec/views/course_spec.coffee | 85 ------------ .../coffee/spec/views/module_edit_spec.coffee | 129 +++++++++--------- .../coffee/spec/views/module_spec.coffee | 24 ---- .../coffee/spec/views/week_edit_spec.coffee | 7 - cms/static/coffee/spec/views/week_spec.coffee | 67 --------- cms/static/coffee/src/main.coffee | 26 ---- .../coffee/src/models/new_module.coffee | 5 - cms/static/coffee/src/views/course.coffee | 28 ---- cms/static/coffee/src/views/module.coffee | 14 -- cms/static/coffee/src/views/module_add.coffee | 26 ---- cms/static/coffee/src/views/week.coffee | 32 ----- cms/static/coffee/src/views/week_edit.coffee | 3 - lms/envs/jasmine.py | 5 +- rakefile | 1 - 17 files changed, 68 insertions(+), 528 deletions(-) delete mode 100644 cms/static/coffee/spec/views/course_spec.coffee delete mode 100644 cms/static/coffee/spec/views/module_spec.coffee delete mode 100644 cms/static/coffee/spec/views/week_edit_spec.coffee delete mode 100644 cms/static/coffee/spec/views/week_spec.coffee delete mode 100644 cms/static/coffee/src/models/new_module.coffee delete mode 100644 cms/static/coffee/src/views/course.coffee delete mode 100644 cms/static/coffee/src/views/module.coffee delete mode 100644 cms/static/coffee/src/views/module_add.coffee delete mode 100644 cms/static/coffee/src/views/week.coffee delete mode 100644 cms/static/coffee/src/views/week_edit.coffee diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index f8ed6e0beb..b29e170411 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -17,8 +17,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ pipeline_group['source_filenames'] - for pipeline_group - in PIPELINE_JS.values() + for group_name, pipeline_group + in PIPELINE_JS.items() + if group_name != 'spec' ], []), 'output_filename': 'js/cms-test-source.js' } diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee index 72800cec7f..8b2fa52866 100644 --- a/cms/static/coffee/spec/main_spec.coffee +++ b/cms/static/coffee/spec/main_spec.coffee @@ -8,72 +8,6 @@ describe "CMS", -> it "should initialize Views", -> expect(CMS.Views).toBeDefined() - describe "start", -> - beforeEach -> - @element = $("
    ") - spyOn(CMS.Views, "Course").andReturn(jasmine.createSpyObj("Course", ["render"])) - CMS.start(@element) - - it "create the Course", -> - expect(CMS.Views.Course).toHaveBeenCalledWith(el: @element) - expect(CMS.Views.Course().render).toHaveBeenCalled() - - describe "view stack", -> - beforeEach -> - @currentView = jasmine.createSpy("currentView") - CMS.viewStack = [@currentView] - - describe "replaceView", -> - beforeEach -> - @newView = jasmine.createSpy("newView") - CMS.on("content.show", (@expectedView) =>) - CMS.replaceView(@newView) - - it "replace the views on the viewStack", -> - expect(CMS.viewStack).toEqual([@newView]) - - it "trigger content.show on CMS", -> - expect(@expectedView).toEqual(@newView) - - describe "pushView", -> - beforeEach -> - @newView = jasmine.createSpy("newView") - CMS.on("content.show", (@expectedView) =>) - CMS.pushView(@newView) - - it "push new view onto viewStack", -> - expect(CMS.viewStack).toEqual([@currentView, @newView]) - - it "trigger content.show on CMS", -> - expect(@expectedView).toEqual(@newView) - - describe "popView", -> - it "remove the current view from the viewStack", -> - CMS.popView() - expect(CMS.viewStack).toEqual([]) - - describe "when there's no view on the viewStack", -> - beforeEach -> - CMS.viewStack = [@currentView] - CMS.on("content.hide", => @eventTriggered = true) - CMS.popView() - - it "trigger content.hide on CMS", -> - expect(@eventTriggered).toBeTruthy - - describe "when there's previous view on the viewStack", -> - beforeEach -> - @parentView = jasmine.createSpyObj("parentView", ["delegateEvents"]) - CMS.viewStack = [@parentView, @currentView] - CMS.on("content.show", (@expectedView) =>) - CMS.popView() - - it "trigger content.show with the previous view on CMS", -> - expect(@expectedView).toEqual @parentView - - it "re-bind events on the view", -> - expect(@parentView.delegateEvents).toHaveBeenCalled() - describe "main helper", -> beforeEach -> @previousAjaxSettings = $.extend(true, {}, $.ajaxSettings) diff --git a/cms/static/coffee/spec/models/module_spec.coffee b/cms/static/coffee/spec/models/module_spec.coffee index 8fd552d93c..5fd447539f 100644 --- a/cms/static/coffee/spec/models/module_spec.coffee +++ b/cms/static/coffee/spec/models/module_spec.coffee @@ -3,75 +3,4 @@ describe "CMS.Models.Module", -> expect(new CMS.Models.Module().url).toEqual("/save_item") it "set the correct default", -> - expect(new CMS.Models.Module().defaults).toEqual({data: ""}) - - describe "loadModule", -> - describe "when the module exists", -> - beforeEach -> - @fakeModule = jasmine.createSpy("fakeModuleObject") - window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule) - @module = new CMS.Models.Module(type: "FakeModule") - @stubDiv = $('
    ') - @stubElement = $('
    ') - @stubElement.data('type', "FakeModule") - - @stubDiv.append(@stubElement) - @module.loadModule(@stubDiv) - - afterEach -> - window.FakeModule = undefined - - it "initialize the module", -> - expect(window.FakeModule).toHaveBeenCalled() - # Need to compare underlying nodes, because jquery selectors - # aren't equal even when they point to the same node. - # http://stackoverflow.com/questions/9505437/how-to-test-jquery-with-jasmine-for-element-id-if-used-as-this - expectedNode = @stubElement[0] - actualNode = window.FakeModule.mostRecentCall.args[0][0] - - expect(actualNode).toEqual(expectedNode) - expect(@module.module).toEqual(@fakeModule) - - describe "when the module does not exists", -> - beforeEach -> - @previousConsole = window.console - window.console = jasmine.createSpyObj("fakeConsole", ["error"]) - @module = new CMS.Models.Module(type: "HTML") - @module.loadModule($("
    ")) - - afterEach -> - window.console = @previousConsole - - it "print out error to log", -> - expect(window.console.error).toHaveBeenCalled() - expect(window.console.error.mostRecentCall.args[0]).toMatch("^Unable to load") - - - describe "editUrl", -> - it "construct the correct URL based on id", -> - expect(new CMS.Models.Module(id: "i4x://mit.edu/module/html_123").editUrl()) - .toEqual("/edit_item?id=i4x%3A%2F%2Fmit.edu%2Fmodule%2Fhtml_123") - - describe "save", -> - beforeEach -> - spyOn(Backbone.Model.prototype, "save") - @module = new CMS.Models.Module() - - describe "when the module exists", -> - beforeEach -> - @module.module = jasmine.createSpyObj("FakeModule", ["save"]) - @module.module.save.andReturn("module data") - @module.save() - - it "set the data and call save on the module", -> - expect(@module.get("data")).toEqual("\"module data\"") - - it "call save on the backbone model", -> - expect(Backbone.Model.prototype.save).toHaveBeenCalled() - - describe "when the module does not exists", -> - beforeEach -> - @module.save() - - it "call save on the backbone model", -> - expect(Backbone.Model.prototype.save).toHaveBeenCalled() + expect(new CMS.Models.Module().defaults).toEqual(undefined) diff --git a/cms/static/coffee/spec/views/course_spec.coffee b/cms/static/coffee/spec/views/course_spec.coffee deleted file mode 100644 index f6a430ac2d..0000000000 --- a/cms/static/coffee/spec/views/course_spec.coffee +++ /dev/null @@ -1,85 +0,0 @@ -describe "CMS.Views.Course", -> - beforeEach -> - setFixtures """ -
    -
    -
      -
    1. -
    2. -
    -
    - """ - CMS.unbind() - - describe "render", -> - beforeEach -> - spyOn(CMS.Views, "Week").andReturn(jasmine.createSpyObj("Week", ["render"])) - new CMS.Views.Course(el: $("#main-section")).render() - - it "create week view for each week",-> - expect(CMS.Views.Week.calls[0].args[0]) - .toEqual({ el: $(".week-one").get(0), height: 101 }) - expect(CMS.Views.Week.calls[1].args[0]) - .toEqual({ el: $(".week-two").get(0), height: 101 }) - - describe "on content.show", -> - beforeEach -> - @view = new CMS.Views.Course(el: $("#main-section")) - @subView = jasmine.createSpyObj("subView", ["render"]) - @subView.render.andReturn(el: "Subview Content") - spyOn(@view, "contentHeight").andReturn(100) - CMS.trigger("content.show", @subView) - - afterEach -> - $("body").removeClass("content") - - it "add content class to body", -> - expect($("body").attr("class")).toEqual("content") - - it "replace content in .main-content", -> - expect($(".main-content")).toHaveHtml("Subview Content") - - it "set height on calendar", -> - expect($(".cal")).toHaveCss(height: "100px") - - it "set minimum height on all sections", -> - expect($("#main-section>section")).toHaveCss(minHeight: "100px") - - describe "on content.hide", -> - beforeEach -> - $("body").addClass("content") - @view = new CMS.Views.Course(el: $("#main-section")) - $(".cal").css(height: 100) - $("#main-section>section").css(minHeight: 100) - CMS.trigger("content.hide") - - afterEach -> - $("body").removeClass("content") - - it "remove content class from body", -> - expect($("body").attr("class")).toEqual("") - - it "remove content from .main-content", -> - expect($(".main-content")).toHaveHtml("") - - it "reset height on calendar", -> - expect($(".cal")).not.toHaveCss(height: "100px") - - it "reset minimum height on all sections", -> - expect($("#main-section>section")).not.toHaveCss(minHeight: "100px") - - describe "maxWeekHeight", -> - it "return maximum height of the week element", -> - @view = new CMS.Views.Course(el: $("#main-section")) - expect(@view.maxWeekHeight()).toEqual(101) - - describe "contentHeight", -> - beforeEach -> - $("body").append($('
    ').height(100).hide()) - - afterEach -> - $("body>header#test").remove() - - it "return the window height minus the header bar", -> - @view = new CMS.Views.Course(el: $("#main-section")) - expect(@view.contentHeight()).toEqual($(window).height() - 100) diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee index 067d169bca..5e83ecb42d 100644 --- a/cms/static/coffee/spec/views/module_edit_spec.coffee +++ b/cms/static/coffee/spec/views/module_edit_spec.coffee @@ -1,81 +1,74 @@ describe "CMS.Views.ModuleEdit", -> beforeEach -> - @stubModule = jasmine.createSpyObj("Module", ["editUrl", "loadModule"]) - spyOn($.fn, "load") + @stubModule = jasmine.createSpy("CMS.Models.Module") + @stubModule.id = 'stub-id' + + setFixtures """ -
    - save - cancel -
      -
    1. - submodule -
    2. -
    +
  1. +
    +
    + ${editor} +
    + Save + Cancel
    - """ #" +
    + Edit + Delete +
    + +
    +
    +
    +
  2. + """ + spyOn($.fn, 'load').andReturn(@moduleData) + + @moduleEdit = new CMS.Views.ModuleEdit( + el: $(".component") + model: @stubModule + onDelete: jasmine.createSpy() + ) CMS.unbind() - describe "defaults", -> - it "set the correct tagName", -> - expect(new CMS.Views.ModuleEdit(model: @stubModule).tagName).toEqual("section") + describe "class definition", -> + it "sets the correct tagName", -> + expect(@moduleEdit.tagName).toEqual("li") - it "set the correct className", -> - expect(new CMS.Views.ModuleEdit(model: @stubModule).className).toEqual("edit-pane") + it "sets the correct className", -> + expect(@moduleEdit.className).toEqual("component") - describe "view creation", -> - beforeEach -> - @stubModule.editUrl.andReturn("/edit_item?id=stub_module") - new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) + describe "methods", -> + describe "initialize", -> + beforeEach -> + spyOn(CMS.Views.ModuleEdit.prototype, 'render') + @moduleEdit = new CMS.Views.ModuleEdit( + el: $(".component") + model: @stubModule + onDelete: jasmine.createSpy() + ) - it "load the edit via ajax and pass to the model", -> - expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function)) - if $.fn.load.mostRecentCall - $.fn.load.mostRecentCall.args[1]() - expect(@stubModule.loadModule).toHaveBeenCalledWith($("#module-edit").get(0)) + it "renders the module editor", -> + expect(@moduleEdit.render).toHaveBeenCalled() - describe "save", -> - beforeEach -> - @stubJqXHR = jasmine.createSpy("stubJqXHR") - @stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR) - @stubJqXHR.error = jasmine.createSpy("stubJqXHR.error").andReturn(@stubJqXHR) - @stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR) - new CMS.Views.ModuleEdit(el: $(".module-edit"), model: @stubModule) - spyOn(window, "alert") - $(".save-update").click() + describe "render", -> + beforeEach -> + spyOn(@moduleEdit, 'loadDisplay') + spyOn(@moduleEdit, 'delegateEvents') + @moduleEdit.render() - it "call save on the model", -> - expect(@stubModule.save).toHaveBeenCalled() + it "loads the module preview and editor via ajax on the view element", -> + expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.id}", jasmine.any(Function)) + @moduleEdit.$el.load.mostRecentCall.args[1]() + expect(@moduleEdit.loadDisplay).toHaveBeenCalled() + expect(@moduleEdit.delegateEvents).toHaveBeenCalled() - it "alert user on success", -> - @stubJqXHR.success.mostRecentCall.args[0]() - expect(window.alert).toHaveBeenCalledWith("Your changes have been saved.") + describe "loadDisplay", -> + beforeEach -> + spyOn(XModule, 'loadModule') + @moduleEdit.loadDisplay() - it "alert user on error", -> - @stubJqXHR.error.mostRecentCall.args[0]() - expect(window.alert).toHaveBeenCalledWith("There was an error saving your changes. Please try again.") - - describe "cancel", -> - beforeEach -> - spyOn(CMS, "popView") - @view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) - $(".cancel").click() - - it "pop current view from viewStack", -> - expect(CMS.popView).toHaveBeenCalled() - - describe "editSubmodule", -> - beforeEach -> - @view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) - spyOn(CMS, "pushView") - spyOn(CMS.Views, "ModuleEdit") - .andReturn(@view = jasmine.createSpy("Views.ModuleEdit")) - spyOn(CMS.Models, "Module") - .andReturn(@model = jasmine.createSpy("Models.Module")) - $(".module-edit").click() - - it "push another module editing view into viewStack", -> - expect(CMS.pushView).toHaveBeenCalledWith @view - expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model - expect(CMS.Models.Module).toHaveBeenCalledWith - id: "i4x://mitx/course/html/module" - type: "html" + it "loads the .xmodule-display inside the module editor", -> + expect(XModule.loadModule).toHaveBeenCalled() + expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) diff --git a/cms/static/coffee/spec/views/module_spec.coffee b/cms/static/coffee/spec/views/module_spec.coffee deleted file mode 100644 index 826263bc41..0000000000 --- a/cms/static/coffee/spec/views/module_spec.coffee +++ /dev/null @@ -1,24 +0,0 @@ -describe "CMS.Views.Module", -> - beforeEach -> - setFixtures """ -
    - edit -
    - """ - - describe "edit", -> - beforeEach -> - @view = new CMS.Views.Module(el: $("#module")) - spyOn(CMS, "replaceView") - spyOn(CMS.Views, "ModuleEdit") - .andReturn(@view = jasmine.createSpy("Views.ModuleEdit")) - spyOn(CMS.Models, "Module") - .andReturn(@model = jasmine.createSpy("Models.Module")) - $(".module-edit").click() - - it "replace the main view with ModuleEdit view", -> - expect(CMS.replaceView).toHaveBeenCalledWith @view - expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model - expect(CMS.Models.Module).toHaveBeenCalledWith - id: "i4x://mitx/course/html/module" - type: "html" diff --git a/cms/static/coffee/spec/views/week_edit_spec.coffee b/cms/static/coffee/spec/views/week_edit_spec.coffee deleted file mode 100644 index 754474d77f..0000000000 --- a/cms/static/coffee/spec/views/week_edit_spec.coffee +++ /dev/null @@ -1,7 +0,0 @@ -describe "CMS.Views.WeekEdit", -> - describe "defaults", -> - it "set the correct tagName", -> - expect(new CMS.Views.WeekEdit().tagName).toEqual("section") - - it "set the correct className", -> - expect(new CMS.Views.WeekEdit().className).toEqual("edit-pane") diff --git a/cms/static/coffee/spec/views/week_spec.coffee b/cms/static/coffee/spec/views/week_spec.coffee deleted file mode 100644 index d5256b0a57..0000000000 --- a/cms/static/coffee/spec/views/week_spec.coffee +++ /dev/null @@ -1,67 +0,0 @@ -describe "CMS.Views.Week", -> - beforeEach -> - setFixtures """ -
    -
    - - edit -
      -
    • -
    • -
    -
    - """ - CMS.unbind() - - describe "render", -> - beforeEach -> - spyOn(CMS.Views, "Module").andReturn(jasmine.createSpyObj("Module", ["render"])) - $.fn.inlineEdit = jasmine.createSpy("$.fn.inlineEdit") - @view = new CMS.Views.Week(el: $("#week"), height: 100).render() - - it "set the height of the element", -> - expect(@view.el).toHaveCss(height: "100px") - - it "make .editable as inline editor", -> - expect($.fn.inlineEdit.calls[0].object.get(0)) - .toEqual($(".editable").get(0)) - - it "make .editable-test as inline editor", -> - expect($.fn.inlineEdit.calls[1].object.get(0)) - .toEqual($(".editable-textarea").get(0)) - - it "create module subview for each module", -> - expect(CMS.Views.Module.calls[0].args[0]) - .toEqual({ el: $("#module-one").get(0) }) - expect(CMS.Views.Module.calls[1].args[0]) - .toEqual({ el: $("#module-two").get(0) }) - - describe "edit", -> - beforeEach -> - new CMS.Views.Week(el: $("#week"), height: 100).render() - spyOn(CMS, "replaceView") - spyOn(CMS.Views, "WeekEdit") - .andReturn(@view = jasmine.createSpy("Views.WeekEdit")) - $(".week-edit").click() - - it "replace the content with edit week view", -> - expect(CMS.replaceView).toHaveBeenCalledWith @view - expect(CMS.Views.WeekEdit).toHaveBeenCalled() - - describe "on content.show", -> - beforeEach -> - @view = new CMS.Views.Week(el: $("#week"), height: 100).render() - @view.$el.height("") - @view.setHeight() - - it "set the correct height", -> - expect(@view.el).toHaveCss(height: "100px") - - describe "on content.hide", -> - beforeEach -> - @view = new CMS.Views.Week(el: $("#week"), height: 100).render() - @view.$el.height("100px") - @view.resetHeight() - - it "remove height from the element", -> - expect(@view.el).not.toHaveCss(height: "100px") diff --git a/cms/static/coffee/src/main.coffee b/cms/static/coffee/src/main.coffee index 57b6d1ae93..8c23d6ac99 100644 --- a/cms/static/coffee/src/main.coffee +++ b/cms/static/coffee/src/main.coffee @@ -6,28 +6,6 @@ AjaxPrefix.addAjaxPrefix(jQuery, -> CMS.prefix) prefix: $("meta[name='path_prefix']").attr('content') - viewStack: [] - - start: (el) -> - new CMS.Views.Course(el: el).render() - - replaceView: (view) -> - @viewStack = [view] - CMS.trigger('content.show', view) - - pushView: (view) -> - @viewStack.push(view) - CMS.trigger('content.show', view) - - popView: -> - @viewStack.pop() - if _.isEmpty(@viewStack) - CMS.trigger('content.hide') - else - view = _.last(@viewStack) - CMS.trigger('content.show', view) - view.delegateEvents() - _.extend CMS, Backbone.Events $ -> @@ -41,7 +19,3 @@ $ -> navigator.userAgent.match /iPhone|iPod|iPad/i $('body').addClass 'touch-based-device' if onTouchBasedDevice() - - - CMS.start($('section.main-container')) - diff --git a/cms/static/coffee/src/models/new_module.coffee b/cms/static/coffee/src/models/new_module.coffee deleted file mode 100644 index 58a109225e..0000000000 --- a/cms/static/coffee/src/models/new_module.coffee +++ /dev/null @@ -1,5 +0,0 @@ -class CMS.Models.NewModule extends Backbone.Model - url: '/clone_item' - - newUrl: -> - "/new_item?#{$.param(parent_location: @get('parent_location'))}" diff --git a/cms/static/coffee/src/views/course.coffee b/cms/static/coffee/src/views/course.coffee deleted file mode 100644 index 2a5a012c07..0000000000 --- a/cms/static/coffee/src/views/course.coffee +++ /dev/null @@ -1,28 +0,0 @@ -class CMS.Views.Course extends Backbone.View - initialize: -> - CMS.on('content.show', @showContent) - CMS.on('content.hide', @hideContent) - - render: -> - @$('#weeks > li').each (index, week) => - new CMS.Views.Week(el: week, height: @maxWeekHeight()).render() - return @ - - showContent: (subview) => - $('body').addClass('content') - @$('.main-content').html(subview.render().el) - @$('.cal').css height: @contentHeight() - @$('>section').css minHeight: @contentHeight() - - hideContent: => - $('body').removeClass('content') - @$('.main-content').empty() - @$('.cal').css height: '' - @$('>section').css minHeight: '' - - maxWeekHeight: -> - weekElementBorderSize = 1 - _.max($('#weeks > li').map -> $(this).height()) + weekElementBorderSize - - contentHeight: -> - $(window).height() - $('body>header').outerHeight() diff --git a/cms/static/coffee/src/views/module.coffee b/cms/static/coffee/src/views/module.coffee deleted file mode 100644 index 1b9e39e8c2..0000000000 --- a/cms/static/coffee/src/views/module.coffee +++ /dev/null @@ -1,14 +0,0 @@ -class CMS.Views.Module extends Backbone.View - events: - "click .module-edit": "edit" - - edit: (event) => - event.preventDefault() - previewType = @$el.data('preview-type') - moduleType = @$el.data('type') - CMS.replaceView new CMS.Views.ModuleEdit - model: new CMS.Models.Module - id: @$el.data('id') - type: if moduleType == 'None' then null else moduleType - previewType: if previewType == 'None' then null else previewType - diff --git a/cms/static/coffee/src/views/module_add.coffee b/cms/static/coffee/src/views/module_add.coffee deleted file mode 100644 index f379174c77..0000000000 --- a/cms/static/coffee/src/views/module_add.coffee +++ /dev/null @@ -1,26 +0,0 @@ -class CMS.Views.ModuleAdd extends Backbone.View - tagName: 'section' - className: 'add-pane' - - events: - 'click .cancel': 'cancel' - 'click .save': 'save' - - initialize: -> - @$el.load @model.newUrl() - - save: (event) -> - event.preventDefault() - @model.save({ - name: @$el.find('.name').val() - template: $(event.target).data('template-id') - }, { - success: -> CMS.popView() - error: -> alert('Create failed') - }) - - cancel: (event) -> - event.preventDefault() - CMS.popView() - - diff --git a/cms/static/coffee/src/views/week.coffee b/cms/static/coffee/src/views/week.coffee deleted file mode 100644 index e2b5a50d59..0000000000 --- a/cms/static/coffee/src/views/week.coffee +++ /dev/null @@ -1,32 +0,0 @@ -class CMS.Views.Week extends Backbone.View - events: - 'click .week-edit': 'edit' - 'click .new-module': 'new' - - initialize: -> - CMS.on('content.show', @resetHeight) - CMS.on('content.hide', @setHeight) - - render: -> - @setHeight() - @$('.editable').inlineEdit() - @$('.editable-textarea').inlineEdit(control: 'textarea') - @$('.modules .module').each -> - new CMS.Views.Module(el: this).render() - return @ - - edit: (event) -> - event.preventDefault() - CMS.replaceView(new CMS.Views.WeekEdit()) - - setHeight: => - @$el.height(@options.height) - - resetHeight: => - @$el.height('') - - new: (event) => - event.preventDefault() - CMS.replaceView new CMS.Views.ModuleAdd - model: new CMS.Models.NewModule - parent_location: @$el.data('id') diff --git a/cms/static/coffee/src/views/week_edit.coffee b/cms/static/coffee/src/views/week_edit.coffee deleted file mode 100644 index 3082bc9fe2..0000000000 --- a/cms/static/coffee/src/views/week_edit.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class CMS.Views.WeekEdit extends Backbone.View - tagName: 'section' - className: 'edit-pane' diff --git a/lms/envs/jasmine.py b/lms/envs/jasmine.py index 14c504e34f..317628f8ba 100644 --- a/lms/envs/jasmine.py +++ b/lms/envs/jasmine.py @@ -17,8 +17,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ pipeline_group['source_filenames'] - for pipeline_group - in PIPELINE_JS.values() + for group_name, pipeline_group + in PIPELINE_JS.items() + if group_name != 'spec' ], []), 'output_filename': 'js/lms-test-source.js' } diff --git a/rakefile b/rakefile index 798898310e..3eb3b7e224 100644 --- a/rakefile +++ b/rakefile @@ -49,7 +49,6 @@ def django_for_jasmine(system, django_reload) django_pid = fork do exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' ')) end - puts django_pid jasmine_url = 'http://localhost:12345/_jasmine/' up = false start_time = Time.now From 87dadd40c400066dcc8973a8f733e1bb6afc217b Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 31 Oct 2012 14:11:24 -0400 Subject: [PATCH 0367/1010] address some of Cale's feedback --- common/lib/xmodule/xmodule/modulestore/__init__.py | 1 + common/lib/xmodule/xmodule/modulestore/xml_importer.py | 10 ++++------ lms/djangoapps/courseware/views.py | 5 +---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index fcbc2d0fbd..55236b4f8e 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -377,6 +377,7 @@ class ModuleStore(object): return courses + class ModuleStoreBase(ModuleStore): ''' Implement interface functionality that can be shared. diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 3c94e25aa2..2677efdb98 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -41,9 +41,8 @@ def import_static_content(modules, data_dir, static_content_store, target_locati content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath) mime_type = mimetypes.guess_type(filename)[0] - f = open(content_path, 'rb') - data = f.read() - f.close() + with open(content_path, 'rb') as f: + data = f.read() content = StaticContent(content_loc, filename, mime_type, data) @@ -76,9 +75,8 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic filename = os.path.basename(path) mime_type = mimetypes.guess_type(filename)[0] - f = open(static_pathname, 'rb') - data = f.read() - f.close() + with open(static_pathname, 'rb') as f: + data = f.read() content = StaticContent(content_loc, filename, mime_type, data) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 14bec50af3..c8ac714084 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -363,10 +363,7 @@ def static_tab(request, course_id, tab_slug): if tab is None: raise Http404 - cache = StudentModuleCache.cache_for_descriptor_descendents( - course.id, request.user, course, depth=2) - - contents = tabs.get_static_tab_contents(request, cache, course, tab) + contents = tabs.get_static_tab_contents(request, None, course, tab) if contents is None: raise Http404 From ef4b2f044f7a0257d505d153d19437949123e7c2 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Wed, 31 Oct 2012 17:03:06 -0400 Subject: [PATCH 0368/1010] =?UTF-8?q?basic=20settings=20framework=20?= =?UTF-8?q?=E2=80=93=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cms/static/sass/_settings.scss | 244 ++++++++++++++++++++++++++++++--- cms/templates/settings.html | 197 ++++++++++++++++++++++---- 2 files changed, 396 insertions(+), 45 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index 7dd349adb9..97d391525b 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -1,39 +1,251 @@ .settings { .settings-overview { @extend .window; - padding: 30px 40px; + @include clearfix; + display: table; + width: 100%; - .details { - margin-bottom: 20px; - font-size: 14px; + .sidebar { + display: table-cell; + float: none; + width: 20%; + padding: 30px 0 30px 20px; + border-radius: 3px 0 0 3px; + background: $lightGrey; } - .row { - margin-bottom: 20px; - padding-bottom: 0; - border-bottom: none; + .main-column { + display: table-cell; + float: none; + width: 80%; + padding: 30px 40px 30px 60px; } label { display: inline-block; width: 200px; - font-size: 14px; + font-size: 15px; + font-weight: 400; &.check-label { display: inline; + margin-left: 10px; + } + + &.cutoff { + float: left; + width: 90px; + line-height: 38px; + } + } + + input { + font-size: 15px; + + &.long { + width: 400px; + } + + &.short { + width: 100px; + } + + &.date { + width: 140px; + } + } + + .settings-page-section { + > section { + display: none; + margin-bottom: 40px; + + &.is-shown { + display: block; + } + + .row { + margin-bottom: 20px; + padding: 0; + border-bottom: none; + + &:last-child { + margin-bottom: 0; + } + } + + &:last-child { + border-bottom: none; + } + + > section { + padding-bottom: 40px; + margin-bottom: 40px; + border-radius: 3px; + border-bottom: 1px solid $mediumGrey; + @include clearfix; + + &:last-child { + padding-bottom: 0; + border-bottom: none; + } + } + } + } + + .settings-page-menu { + a { + display: block; + padding-left: 20px; + line-height: 52px; + + &.is-shown { + background: #fff; + border-radius: 5px 0 0 5px; + } } } } h2 { - margin-bottom: 20px; - font-size: 24px; - font-weight: 300; + margin-bottom: 30px; + font-size: 28px; + font-weight: 300; + color: $blue; } - section { - border-bottom: 1px solid $mediumGrey; - margin-bottom: 40px; - padding-bottom: 40px; + h3 { + margin-bottom: 20px; + font-size: 15px; + font-weight: 700; + line-height: 34px; + color: $blue; + } + + .grade-slider { + float: left; + width: 500px; + + .grade-bar { + position: relative; + width: 100%; + height: 40px; + background: $lightGrey; + + .increments { + position: relative; + + li { + position: absolute; + top: 42px; + width: 30px; + margin-left: -15px; + font-size: 9px; + text-align: center; + + &.increment-0 { + left: 0; + } + + &.increment-10 { + left: 10%; + } + + &.increment-20 { + left: 20%; + } + + &.increment-30 { + left: 30%; + } + + &.increment-40 { + left: 40%; + } + + &.increment-50 { + left: 50%; + } + + &.increment-60 { + left: 60%; + } + + &.increment-70 { + left: 70%; + } + + &.increment-80 { + left: 80%; + } + + &.increment-90 { + left: 90%; + } + + &.increment-100 { + left: 100%; + } + } + } + + .grades { + position: relative; + + li { + position: absolute; + top: 0; + height: 40px; + text-align: right; + color: rgba(0, 0, 0, .5); + + &.bar-a { + background: #4fe696; + width: 100%; + } + + &.bar-b { + background: #ffdf7e; + width: 80%; + } + + &.bar-c { + background: #ef68a6; + width: 70%; + } + + .letter-grade { + display: block; + margin: 6px 5px 0 0; + font-size: 14px; + font-weight: 700; + line-height: 14px; + } + + .range { + display: block; + margin-right: 5px; + font-size: 10px; + line-height: 12px; + } + + .drag-bar { + position: absolute; + top: 0; + right: -1px; + height: 40px; + width: 2px; + background-color: #fff; + cursor: ew-resize; + @include transition(none); + + &:hover { + width: 4px; + right: -2px; + } + } + } + } + } } } \ No newline at end of file diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 8df3cede31..0d8f7f05b0 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -6,42 +6,181 @@ <%namespace name='static' file='static_content.html'/> <%block name="jsextra"> + <%block name="content">
    -
    +

    Settings

    -
    -

    Details

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

    Grading

    -
    - - -
    -
    -
    -

    Problems

    -
    - -
    -
    + +
    +
    +

    Course Details

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +

    Grading

    +
    + +
    +
    +
      +
    1. 0
    2. +
    3. 10
    4. +
    5. 20
    6. +
    7. 30
    8. +
    9. 40
    10. +
    11. 50
    12. +
    13. 60
    14. +
    15. 70
    16. +
    17. 80
    18. +
    19. 90
    20. +
    21. 100
    22. +
    +
      +
    1. + A + 80–100 +
    2. +
    3. + B + 70–80 + +
    4. +
    5. + C + 0–70 + +
    6. +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    Homework

    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +

    Lab

    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +

    Problems

    +
    + +
    +
    +
    From 17528906abd84e0bdb8aac10f0da3f7e55d92b2a Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 31 Oct 2012 22:16:11 -0400 Subject: [PATCH 0369/1010] make sure paths are path() objects rather than strings so that the '/' operator works as expected. Also some path cleanup and remove duplicate computation --- .../xmodule/modulestore/xml_importer.py | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 2677efdb98..00ddb6a948 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -2,6 +2,7 @@ import logging import os import mimetypes from lxml.html import rewrite_links as lxml_rewrite_links +from path import path from .xml import XMLModuleStore from .exceptions import DuplicateItemError @@ -10,27 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX log = logging.getLogger(__name__) -def import_static_content(modules, data_dir, static_content_store, target_location_namespace): +def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace): remap_dict = {} - - course_data_dir = None - course_loc = None - - # quick scan to find the course module and pull out the data_dir and location - # maybe there an easier way to look this up?!? - - for module in modules.itervalues(): - if module.category == 'course': - course_loc = module.location - course_data_dir = module.metadata['data_dir'] - - if course_data_dir is None or course_loc is None: - return remap_dict - # now import all static assets - static_dir = '{0}/static/'.format(data_dir / course_data_dir) + static_dir = course_data_path / 'static' for dirname, dirnames, filenames in os.walk(static_dir): for filename in filenames: @@ -130,13 +116,16 @@ def import_from_xml(store, data_dir, course_dirs=None, course_items = [] for course_id in module_store.modules.keys(): - course_data_dir = None + course_data_path = None + course_location = None + # Quick scan to get course Location as well as the course_data_path for module in module_store.modules[course_id].itervalues(): if module.category == 'course': - course_data_dir = module.metadata['data_dir'] + course_data_path = path(data_dir) / module.metadata['data_dir'] + course_location = module.location if static_content_store is not None: - import_static_content(module_store.modules[course_id], data_dir, static_content_store, + import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store, target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location) for module in module_store.modules[course_id].itervalues(): @@ -172,7 +161,7 @@ def import_from_xml(store, data_dir, course_dirs=None, # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg # so let's make sure we import in case there are no other references to it in the modules - verify_content_links(module, data_dir / course_data_dir, static_content_store, '/static/images/course_image.jpg') + verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg') course_items.append(module) @@ -192,7 +181,7 @@ def import_from_xml(store, data_dir, course_dirs=None, # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # no good, so we have to do this kludge if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code - lxml_rewrite_links(module_data, lambda link: verify_content_links(module, data_dir / course_data_dir, + lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict)) for key in remap_dict.keys(): From a6d5edce5e871261d0ee4c5ed91089c8e1a0c10a Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 1 Nov 2012 11:34:02 -0400 Subject: [PATCH 0370/1010] polished grade range slider --- cms/static/sass/_settings.scss | 79 +++++++++++++++++++------ cms/templates/settings.html | 105 +++++++++++++++++++++------------ 2 files changed, 128 insertions(+), 56 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index 97d391525b..0834a90893 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -32,10 +32,8 @@ margin-left: 10px; } - &.cutoff { - float: left; - width: 90px; - line-height: 38px; + &.ranges { + margin-bottom: 20px; } } @@ -115,16 +113,43 @@ } h3 { - margin-bottom: 20px; + margin-bottom: 30px; font-size: 15px; font-weight: 700; - line-height: 34px; color: $blue; } + .grade-controls { + @include clearfix; + } + + .new-grade-button { + position: relative; + float: left; + display: block; + width: 29px; + height: 29px; + margin: 4px 10px 0 0; + border-radius: 20px; + border: 1px solid $darkGrey; + @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); + background-color: #d1dae3; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + color: #6d788b; + + .plus-icon { + position: absolute; + top: 50%; + left: 50%; + margin-left: -6px; + margin-top: -6px; + } + } + .grade-slider { float: left; - width: 500px; + width: 560px; + height: 60px; .grade-bar { position: relative; @@ -197,26 +222,46 @@ top: 0; height: 40px; text-align: right; - color: rgba(0, 0, 0, .5); - &.bar-a { + &:hover, + &.is-dragging { + .remove-button { + display: block; + } + } + + .remove-button { + display: none; + position: absolute; + top: -17px; + right: 1px; + height: 17px; + font-size: 10px; + } + + &:nth-child(1) { background: #4fe696; - width: 100%; } - &.bar-b { + &:nth-child(2) { background: #ffdf7e; - width: 80%; } - &.bar-c { - background: #ef68a6; - width: 70%; + &:nth-child(3) { + background: #ffb657; + } + + &:nth-child(4) { + background: #fb336c; + } + + &:nth-child(5) { + background: #ef54a1; } .letter-grade { display: block; - margin: 6px 5px 0 0; + margin: 7px 5px 0 0; font-size: 14px; font-weight: 700; line-height: 14px; @@ -225,7 +270,7 @@ .range { display: block; margin-right: 5px; - font-size: 10px; + font-size: 9px; line-height: 12px; } diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 0d8f7f05b0..1630ad4ffa 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -13,15 +13,31 @@ var barOrigin; var barWidth; var gradeThresholds; + var GRADES = ['A', 'B', 'C', 'D', 'E']; (function() { $body = $('body'); $gradeBar = $('.grade-bar'); gradeThresholds = [100, 80, 70]; $('.settings-page-menu a').bind('click', showSettingsTab); - $('.drag-bar').bind('mousedown', startDragBar); + $body.on('mousedown', '.drag-bar', startDragBar); + $('.new-grade-button').bind('click', addNewGrade); + $body.on('click', '.remove-button', removeGrade); })(); + function addNewGrade(e) { + e.preventDefault(); + var $newGradeBar = $('
  3. ' + GRADES[$('.grades li').length] + 'remove
  4. '); + $('.grades').append($newGradeBar); + } + + function removeGrade(e) { + e.preventDefault(); + var index = $(this).closest('li').index(); + gradeThresholds.splice(index, 1); + $(this).closest('li').remove(); + } + function showSettingsTab(e) { e.preventDefault(); $('.settings-page-section > section').hide(); @@ -34,28 +50,33 @@ e.preventDefault(); barOrigin = $gradeBar.offset().left; barWidth = $gradeBar.width(); - $draggingBar = $(e.target).closest('li'); + $draggingBar = $(e.target).closest('li').addClass('is-dragging'); $body.bind('mousemove', moveBar); $body.bind('mouseup', stopDragBar); } function moveBar(e) { - var percentage = (e.pageX - barOrigin) / barWidth * 100; + var barIndex = $draggingBar.index(); + console.log(barIndex); + var min = gradeThresholds[barIndex + 1] || 0; + var max = gradeThresholds[barIndex - 1] || 100; + var percentage = Math.min(Math.max((e.pageX - barOrigin) / barWidth * 100, min), max); $draggingBar.css('width', percentage + '%'); gradeThresholds[$draggingBar.index()] = Math.round(percentage); renderGradeRanges(); } function stopDragBar(e) { + $draggingBar.removeClass('is-dragging'); $body.unbind('mousemove', moveBar); $body.unbind('mouseup', stopDragBar); } function renderGradeRanges() { $('.range').each(function(i) { - var min = gradeThresholds[i + 1] || 0; + var min = gradeThresholds[i + 1] + 1 || 0; var max = gradeThresholds[i]; - $(this).text(min + '–' + max); + $(this).text(min + '-' + max); }); } @@ -76,7 +97,7 @@
    -
    +

    Course Details

    @@ -99,41 +120,47 @@
    -
    +

    Grading

    - -
    -
    -
      -
    1. 0
    2. -
    3. 10
    4. -
    5. 20
    6. -
    7. 30
    8. -
    9. 40
    10. -
    11. 50
    12. -
    13. 60
    14. -
    15. 70
    16. -
    17. 80
    18. -
    19. 90
    20. -
    21. 100
    22. -
    -
      -
    1. - A - 80–100 -
    2. -
    3. - B - 70–80 - -
    4. -
    5. - C - 0–70 - -
    6. -
    + +
    + +
    +
    +
      +
    1. 0
    2. +
    3. 10
    4. +
    5. 20
    6. +
    7. 30
    8. +
    9. 40
    10. +
    11. 50
    12. +
    13. 60
    14. +
    15. 70
    16. +
    17. 80
    18. +
    19. 90
    20. +
    21. 100
    22. +
    +
      +
    1. + A + 81-100 + remove +
    2. +
    3. + B + 71-80 + + remove +
    4. +
    5. + C + 0-70 + + remove +
    6. +
    +
    From 7887f92e3ba50517eb388e94e51257c395986b52 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 1 Nov 2012 11:34:31 -0400 Subject: [PATCH 0371/1010] removed log --- cms/templates/settings.html | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 1630ad4ffa..4110f72ef4 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -57,7 +57,6 @@ function moveBar(e) { var barIndex = $draggingBar.index(); - console.log(barIndex); var min = gradeThresholds[barIndex + 1] || 0; var max = gradeThresholds[barIndex - 1] || 100; var percentage = Math.min(Math.max((e.pageX - barOrigin) / barWidth * 100, min), max); From 8ed4ef44123d1846fe4e306bf0483be480beb0ec Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 1 Nov 2012 11:36:41 -0400 Subject: [PATCH 0372/1010] fixed typo --- .../lib/xmodule/xmodule/templates/problem/latex_problem.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml index 6b31c4af3e..434354e4c7 100644 --- a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml @@ -36,7 +36,7 @@ metadata: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \subsection{Example "multiple choice" problem} - What color is a bannana? + What color is a banana? \edXabox{ type="multichoice" expect="Yellow" options="Red","Green","Yellow","Blue" } @@ -129,7 +129,7 @@ data: |

    Example "multiple choice" problem

    - What color is a bannana?

    + What color is a banana?

    From b788b9d6591653f5f9f455dad7d946f2739fc18a Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 1 Nov 2012 15:08:26 -0400 Subject: [PATCH 0373/1010] add to existing test cases to exercise the 'course extras as modules' work in the CMS import. Also add to the existing 'full' test data collection to include policy, tabs, etc. --- common/lib/xmodule/xmodule/xml_module.py | 7 ++- common/test/data/full/grading_policy.js | 22 +++++++++ .../data/full/policies/6.002_Spring_2012.json | 16 +++++++ common/test/data/full/tabs/syllabus.html | 1 + lms/djangoapps/courseware/courses.py | 3 +- lms/djangoapps/courseware/module_render.py | 8 +++- lms/djangoapps/courseware/tests/tests.py | 47 +++++++++++++++---- 7 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 common/test/data/full/grading_policy.js create mode 100644 common/test/data/full/policies/6.002_Spring_2012.json create mode 100644 common/test/data/full/tabs/syllabus.html diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index a5b3460f17..24bbe40c13 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -93,7 +93,9 @@ class XmlDescriptor(XModuleDescriptor): # VS[compat] Remove once unused. 'name', 'slug') - metadata_to_strip = ('data_dir', + metadata_to_strip = ('data_dir', + # cdodge: @TODO: We need to figure out a way to export out 'tabs' and 'grading_policy' which is on the course + 'tabs', 'grading_policy', # VS[compat] -- remove the below attrs once everything is in the CMS 'course', 'org', 'url_name', 'filename') @@ -355,7 +357,8 @@ class XmlDescriptor(XModuleDescriptor): for attr in sorted(self.own_metadata): # don't want e.g. data_dir if attr not in self.metadata_to_strip: - xml_object.set(attr, val_for_xml(attr)) + val = val_for_xml(attr) + xml_object.set(attr, val) if self.export_to_file(): # Write the definition to a file diff --git a/common/test/data/full/grading_policy.js b/common/test/data/full/grading_policy.js new file mode 100644 index 0000000000..e48627e711 --- /dev/null +++ b/common/test/data/full/grading_policy.js @@ -0,0 +1,22 @@ +{ + "GRADER" : [ + { + "type" : "Homework", + "min_count" : 3, + "drop_count" : 1, + "short_label" : "HW", + "weight" : 0.5 + }, + { + "type" : "Final", + "name" : "Final Question", + "short_label" : "Final", + "weight" : 0.5 + } + ], + "GRADE_CUTOFFS" : { + "A" : 0.8, + "B" : 0.7, + "C" : 0.44 + } +} diff --git a/common/test/data/full/policies/6.002_Spring_2012.json b/common/test/data/full/policies/6.002_Spring_2012.json new file mode 100644 index 0000000000..345309ff5c --- /dev/null +++ b/common/test/data/full/policies/6.002_Spring_2012.json @@ -0,0 +1,16 @@ +{ + "course/6.002_Spring_2012": { + "graceperiod": "1 day 12 hours 59 minutes 59 seconds", + "start": "2012-09-21T12:00", + "display_name": "Testing", + "xqa_key": "5HapHs6tHhu1iN1ZX5JGNYKRkXrXh7NC", + "tabs": [ + {"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "static_tab", "url_slug": "syllabus", "name": "Syllabus"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"} + ] + } +} \ No newline at end of file diff --git a/common/test/data/full/tabs/syllabus.html b/common/test/data/full/tabs/syllabus.html new file mode 100644 index 0000000000..72241b39b3 --- /dev/null +++ b/common/test/data/full/tabs/syllabus.html @@ -0,0 +1 @@ +

    This is a sample tab

    \ No newline at end of file diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index aa3fbf12bb..6300b2c491 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -23,6 +23,7 @@ from static_replace import replace_urls, try_staticfiles_lookup from courseware.access import has_access import branding from courseware.models import StudentModuleCache +from xmodule.modulestore.exceptions import ItemNotFoundError log = logging.getLogger(__name__) @@ -147,7 +148,7 @@ def get_course_about_section(course, section_key): request = get_request_for_thread() loc = course.location._replace(category='about', name=section_key) - course_module = get_module(request.user, request, loc, None, course.id) + course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True) html = '' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d9f87d77b6..5e79226a21 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -28,6 +28,8 @@ from xmodule.x_module import ModuleSystem from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule +from xmodule.modulestore.exceptions import ItemNotFoundError + log = logging.getLogger("mitx.courseware") @@ -112,7 +114,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): return chapters -def get_module(user, request, location, student_module_cache, course_id, position=None): +def get_module(user, request, location, student_module_cache, course_id, position=None, not_found_ok = False): """ Get an instance of the xmodule class identified by location, setting the state based on an existing StudentModule, or creating one if none @@ -134,6 +136,10 @@ def get_module(user, request, location, student_module_cache, course_id, positio """ try: return _get_module(user, request, location, student_module_cache, course_id, position) + except ItemNotFoundError: + if not not_found_ok: + log.exception("Error in get_module") + return None except: # Something has gone terribly wrong, but still not letting it turn into a 500. log.exception("Error in get_module") diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 8bdcf59815..5af79d4983 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -250,20 +250,47 @@ class PageLoader(ActivateLoginTestCase): n += 1 print "Checking ", descriptor.location.url() - #print descriptor.__class__, descriptor.location - resp = self.client.get(reverse('jump_to', - kwargs={'course_id': course_id, - 'location': descriptor.location.url()})) - msg = str(resp.status_code) - if resp.status_code != 302: - # cdodge: we're adding new module category as part of the Studio work - # such as 'course_info', etc, which are not supposed to be jump_to'able - # so a successful return value here should be a 404 - if descriptor.location.category not in ['about', 'static_tab', 'course_info', 'custom_tag_template'] or resp.status_code != 404: + # We have ancillary course information now as modules and we can't simply use 'jump_to' to view them + if descriptor.location.category == 'about': + resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id})) + msg = str(resp.status_code) + + if resp.status_code != 200: msg = "ERROR " + msg all_ok = False num_bad += 1 + elif descriptor.location.category == 'static_tab': + resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug' : descriptor.location.name})) + msg = str(resp.status_code) + + if resp.status_code != 200: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 + elif descriptor.location.category == 'course_info': + resp = self.client.get(reverse('info', kwargs={'course_id': course_id})) + msg = str(resp.status_code) + + if resp.status_code != 200: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 + else: + #print descriptor.__class__, descriptor.location + resp = self.client.get(reverse('jump_to', + kwargs={'course_id': course_id, + 'location': descriptor.location.url()})) + msg = str(resp.status_code) + + if resp.status_code != 302: + # cdodge: we're adding 'custom_tag_template' which is the Mako template used to render + # the custom tag. We can't 'jump-to' this module. Unfortunately, we also can't test render + # it easily + if descriptor.location.category not in ['custom_tag_template'] or resp.status_code != 404: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 print msg self.assertTrue(all_ok) # fail fast From b75a814f7ae12aeb83a5fea793abd6689509aaa8 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 2 Nov 2012 15:08:45 -0400 Subject: [PATCH 0374/1010] add django performance metering --- lms/envs/cms/dev.py | 10 ++++++++++ requirements.txt | 2 ++ 2 files changed, 12 insertions(+) diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py index 3d2c0c3c3b..4b6b0a12f0 100644 --- a/lms/envs/cms/dev.py +++ b/lms/envs/cms/dev.py @@ -33,3 +33,13 @@ CONTENTSTORE = { 'db': 'xcontent', } } + +INSTALLED_APPS += ( + # Mongo perf stats + 'debug_toolbar_mongo', + ) + + +DEBUG_TOOLBAR_PANELS += ( + 'debug_toolbar_mongo.panel.MongoDebugPanel', + ) diff --git a/requirements.txt b/requirements.txt index 2ebca50bc5..c1738bf98c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,3 +50,5 @@ pygraphviz -r repo-requirements.txt pil nltk +django-debug-toolbar-mongo + From 6b45b4c741ec747c02e15035e6897397dd8d8e00 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 5 Nov 2012 12:02:08 -0500 Subject: [PATCH 0375/1010] bind the disabling/enabling of the unit name input field based on whether the page state is public or not. We shouldn't be able to edit when public. --- cms/djangoapps/contentstore/views.py | 7 +++++-- cms/static/coffee/src/views/unit.coffee | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 05cde6043d..ab16958cf3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -580,8 +580,11 @@ def save_item(request): if request.POST.get('data') is not None: data = request.POST['data'] store.update_item(item_location, data) - - if request.POST.get('children') is not None: + + # cdodge: note calling request.POST.get('children') will return None if children is an empty array + # so it lead to a bug whereby the last component to be deleted in the UI was not actually + # deleting the children object from the children collection + if 'children' in request.POST and request.POST['children'] is not None: children = request.POST['children'] store.update_children(item_location, children) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 83deb0f549..f32e35c9bd 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -169,12 +169,21 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View initialize: => @model.on('change:metadata', @render) + @model.on('change:state', @setEnabled) + @setEnabled() @saveName @$spinner = $(''); render: => @$('.unit-display-name-input').val(@model.get('metadata').display_name) + setEnabled: => + disabled = @model.get('state') == 'public' + if disabled + @$('.unit-display-name-input').attr('disabled', true) + else + @$('.unit-display-name-input').removeAttr('disabled') + saveName: => # Treat the metadata dictionary as immutable metadata = $.extend({}, @model.get('metadata')) @@ -196,7 +205,7 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View if @timer clearTimeout @timer @timer = setTimeout( => - @model.save(metadata: metadata) + @model.save(metadata: metadata, children: null) @timer = null @$spinner.delay(500).fadeOut(150) , 500) From a9c4493a70129aee3a0dddec157b575a1dee1fa4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 5 Nov 2012 12:26:44 -0500 Subject: [PATCH 0376/1010] always make the spinner spin on unit renames --- cms/static/coffee/src/views/unit.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index f32e35c9bd..0010e4b8f2 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -200,6 +200,7 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View 'margin-top': '-10px' }); inputField.after(@$spinner); + @$spinner.fadeIn(10) # save the name after a slight delay if @timer From 3f01bef1ebb8a2067601dc49ab64bc9696b14a05 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 6 Nov 2012 10:34:51 -0500 Subject: [PATCH 0377/1010] Add changelog information --- cms/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/CHANGELOG.md b/cms/CHANGELOG.md index bdd325bd8d..d21d08d23c 100644 --- a/cms/CHANGELOG.md +++ b/cms/CHANGELOG.md @@ -16,4 +16,6 @@ Changes Upcoming -------- -* Created changelog \ No newline at end of file +* Fix: Deleting last component in a unit does not work +* Fix: Unit name is editable when a unit is public +* Fix: Visual feedback inconsistent when saving a unit name change From afd85845694c4ff81ed1715209876e56a9e8ba31 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 7 Nov 2012 09:34:44 -0500 Subject: [PATCH 0378/1010] remove explicit 'null' for child collection on saveName on unit code paths. I had seen cases where save was posting back an empty set on renames - deleting the components. But I can no longer reproduce it now. --- cms/static/coffee/src/views/unit.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 0010e4b8f2..a250925bf9 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -206,7 +206,7 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View if @timer clearTimeout @timer @timer = setTimeout( => - @model.save(metadata: metadata, children: null) + @model.save(metadata: metadata) @timer = null @$spinner.delay(500).fadeOut(150) , 500) From 3b42ea7e9e037e2f21e2ab2443fb580d1a0bee57 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 7 Nov 2012 15:00:48 -0500 Subject: [PATCH 0379/1010] added basic settings markup and revised sections based on additional fields/info needed --- cms/static/sass/_base.scss | 3 +- cms/static/sass/_settings.scss | 14 +- cms/templates/settings.html | 227 ++++++++++++++++++++++++++++++--- 3 files changed, 222 insertions(+), 22 deletions(-) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index c1875edb06..a91d703d0f 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -80,7 +80,8 @@ footer { input[type="text"], input[type="email"], -input[type="password"] { +input[type="password"], +textarea { padding: 6px 8px 8px; @include box-sizing(border-box); border: 1px solid #b0b6c2; diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index 0834a90893..e26bf8dd8d 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -37,13 +37,17 @@ } } - input { + input, textarea { font-size: 15px; &.long { width: 400px; } + &.tall { + height: 200px; + } + &.short { width: 100px; } @@ -54,6 +58,14 @@ } .settings-page-section { + > .alert { + display: none; + + &.is-shown { + display: block; + } + } + > section { display: none; margin-bottom: 40px; diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 4110f72ef4..fa5443a0c0 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -90,35 +90,211 @@
    +

    Course Details

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    + +
    +
    +

    Basic Information

    + The nuts and bolts of your class +
    + +
    + + +
    +
    + + +
    +
    + + + e.g. 101x +
    +
    + + +
    +
    + +
    +
    +

    Dates & Times

    + The nuts and bolts of your class +
    + +
    + + + First day the class begins +
    + +
    + + + Last day of class activty +
    + +
    + + +
    +
      +
    1. +
      + + Milestone Date +
      + +
      + + Milestone Name +
      + + +
    2. +
    + + + New Class Milestone + +
    +
    +
    + +
    +
    +

    Introducing Your Course

    + How your course will be shown to students considering taking it +
    + +
    + + +
    + +
    + + + Used to introduce your class to perspective students +
    + +
    + + +
    +
    + +
    +
    +

    Requirements

    + Expectations of the students taking this course +
    + +
    + + +
    + +
    + + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
      +
    1. +
      + + Textbook Name +
      + +
      + + Textbook URL +
      + + +
    2. +
    + + + New Textbook + +
    +
    +
    + +
    +
    +

    More Information

    + Other helpful information about the course +
    + +
    + + +
    +
      +
    1. +
      + + Question +
      + +
      + + Answer +
      + + +
    2. +
    + + + New Question & Answer + +
    +
    +
    +
    + +
    +

    Course Staff

    + +
    +

    Grading

    @@ -200,6 +376,17 @@
    + +
    +

    Handouts & Guides

    + +
    + + + PDF formatted file +
    +
    +

    Problems

    From 9cc1af5052c182255b49dc39e6bfbde2e1328f1e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 7 Nov 2012 15:27:44 -0500 Subject: [PATCH 0380/1010] first pass at static tab editing. Doesn't yet support drag/drop as well as new tab creation, which needs to modify the policy on the course --- cms/djangoapps/contentstore/views.py | 23 ++++++++++++++++++++++- cms/templates/widgets/header.html | 2 +- cms/urls.py | 1 + common/lib/xmodule/setup.py | 6 +++--- common/lib/xmodule/xmodule/html_module.py | 22 ++++++++++++++++++++++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b3af6f7e7b..5495b5ea0b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -66,7 +66,7 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] -DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential'] +DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] def _modulestore(location): @@ -870,6 +870,27 @@ def edit_static(request, org, course, coursename): return render_to_response('edit-static-page.html', {}) +def edit_tabs(request, org, course, coursename): + location = ['i4x', org, course, 'course', coursename] + course_item = modulestore().get_item(location) + static_tabs_loc = Location('i4x', org, course, 'static_tab', None) + + static_tabs = modulestore('direct').get_items(static_tabs_loc) + + # import pudb; pudb.set_trace() + + components = [ + static_tab.location.url() + for static_tab + in static_tabs + ] + + return render_to_response('edit-tabs.html', { + 'active_tab': 'pages', + 'context_course':course_item, + 'components': components + }) + def not_found(request): return render_to_response('error.html', {'error': '404'}) diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 2b9b2c7884..0f5780a5d2 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -10,7 +10,7 @@ ${context_course.display_name}
    • Courseware
    • -
    • +
    • Tabs
    • Assets
    • Users
    • Import
    • diff --git a/cms/urls.py b/cms/urls.py index 7b3dd90a0b..0519debbe0 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -36,6 +36,7 @@ urlpatterns = ('', 'contentstore.views.remove_user', name='remove_user'), url(r'^pages/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.static_pages', name='static_pages'), url(r'^edit_static/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_static', name='edit_static'), + url(r'^edit_tabs/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'), url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$', 'contentstore.views.asset_index', name='asset_index'), # temporary landing page for a course diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index ba5bcd872f..74fa418c91 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -35,10 +35,10 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", - "course_info = xmodule.html_module:HtmlDescriptor", - "static_tab = xmodule.html_module:HtmlDescriptor", + "course_info = xmodule.html_module:CourseInfoDescriptor", + "static_tab = xmodule.html_module:StaticTabDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor", - "about = xmodule.html_module:HtmlDescriptor" + "about = xmodule.html_module:AboutDescriptor" ] } ) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index f6dddfdd4c..cae099845a 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -170,3 +170,25 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): elt = etree.Element('html') elt.set("filename", relname) return elt + + +class AboutDescriptor(HtmlDescriptor): + """ + These pieces of course content are treated as HtmlModules but we need to overload where the templates are located + in order to be able to create new ones + """ + template_dir_name = "about" + +class StaticTabDescriptor(HtmlDescriptor): + """ + These pieces of course content are treated as HtmlModules but we need to overload where the templates are located + in order to be able to create new ones + """ + template_dir_name = "statictab" + +class CourseInfoDescriptor(HtmlDescriptor): + """ + These pieces of course content are treated as HtmlModules but we need to overload where the templates are located + in order to be able to create new ones + """ + template_dir_name = "courseinfo" From a99277275d28de339cdeb3d4707a74532397ef90 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 7 Nov 2012 15:56:16 -0500 Subject: [PATCH 0381/1010] add new files --- cms/static/coffee/src/views/tabs.coffee | 54 +++++++++++++++++++ cms/templates/edit-tabs.html | 40 ++++++++++++++ .../xmodule/templates/about/empty.yaml | 5 ++ .../xmodule/templates/courseinfo/empty.yaml | 5 ++ .../xmodule/templates/statictab/empty.yaml | 5 ++ 5 files changed, 109 insertions(+) create mode 100644 cms/static/coffee/src/views/tabs.coffee create mode 100644 cms/templates/edit-tabs.html create mode 100644 common/lib/xmodule/xmodule/templates/about/empty.yaml create mode 100644 common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml create mode 100644 common/lib/xmodule/xmodule/templates/statictab/empty.yaml diff --git a/cms/static/coffee/src/views/tabs.coffee b/cms/static/coffee/src/views/tabs.coffee new file mode 100644 index 0000000000..9797b9495b --- /dev/null +++ b/cms/static/coffee/src/views/tabs.coffee @@ -0,0 +1,54 @@ +class CMS.Views.TabsEdit extends Backbone.View + events: + 'click .new-tab': 'addNewTab' + + initialize: => + @$('.component').each((idx, element) => + new CMS.Views.ModuleEdit( + el: element, + onDelete: @deleteTab, + model: new CMS.Models.Module( + id: $(element).data('id'), + ) + ) + ) + + @$('.components').sortable( + handle: '.drag-handle' + update: (event, ui) => alert 'got it!' + helper: 'clone' + opacity: '0.5' + placeholder: 'component-placeholder' + forcePlaceholderSize: true + axis: 'y' + items: '> .component' + ) + + addNewTab: (event) => + event.preventDefault() + + editor = new CMS.Views.ModuleEdit( + onDelete: @deleteTab + model: new CMS.Models.Module() + ) + + $('.new-component-item').before(editor.$el) + + editor.cloneTemplate( + @model.get('id'), + 'i4x://edx/templates/static_tab/Empty' + ) + + deleteTab: (event) => + if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' + return + $component = $(event.currentTarget).parents('.component') + $.post('/delete_item', { + id: $component.data('id') + }, => + $component.remove() + ) + + + + diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html new file mode 100644 index 0000000000..7c4fc57ec2 --- /dev/null +++ b/cms/templates/edit-tabs.html @@ -0,0 +1,40 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Tabs +<%block name="bodyclass">static-pages + +<%block name="jsextra"> + + + +<%block name="content"> +
      +
      +

      Static Tabs

      +
      +
      +
      +
        + % for id in components: +
      1. + % endfor + +
      2. + + New Tab + +
      3. +
      +
      +
      +
      +
      +
      + \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/about/empty.yaml b/common/lib/xmodule/xmodule/templates/about/empty.yaml new file mode 100644 index 0000000000..71bcef8914 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/about/empty.yaml @@ -0,0 +1,5 @@ +--- +metadata: + display_name: Empty +data: "This is where you can add additional information about your course." +children: [] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml new file mode 100644 index 0000000000..71bcef8914 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml @@ -0,0 +1,5 @@ +--- +metadata: + display_name: Empty +data: "This is where you can add additional information about your course." +children: [] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml new file mode 100644 index 0000000000..31306e2ce6 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml @@ -0,0 +1,5 @@ +--- +metadata: + display_name: Empty +data: "This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing." +children: [] \ No newline at end of file From 7dbabce4db2d1bce7b7f272631c18b02f168092d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 8 Nov 2012 09:55:52 -0500 Subject: [PATCH 0382/1010] fix up styling by reusing existing CSS class names. Also refactor some code regarding the management of static_tabs so that it's in the course as course contains references to static_tabs in the current data model --- cms/djangoapps/contentstore/utils.py | 25 +++++++++++++ cms/djangoapps/contentstore/views.py | 40 +++++++++++++++++++-- cms/static/coffee/src/views/tabs.coffee | 2 +- cms/templates/edit-tabs.html | 4 ++- common/lib/xmodule/xmodule/course_module.py | 27 ++++++++++++++ 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 18afd331d0..508236a1e9 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -33,6 +33,31 @@ def get_course_location_for_item(location): return location +def get_course_for_item(location): + ''' + cdodge: for a given Xmodule, return the course that it belongs to + NOTE: This makes a lot of assumptions about the format of the course location + Also we have to assert that this module maps to only one course item - it'll throw an + assert if not + ''' + item_loc = Location(location) + + # @hack! We need to find the course location however, we don't + # know the 'name' parameter in this context, so we have + # to assume there's only one item in this query even though we are not specifying a name + course_search_location = ['i4x', item_loc.org, item_loc.course, 'course', None] + courses = modulestore().get_items(course_search_location) + + # make sure we found exactly one match on this above course search + found_cnt = len(courses) + if found_cnt == 0: + raise BaseException('Could not find course at {0}'.format(course_search_location)) + + if found_cnt > 1: + raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses)) + + return courses[0] + def get_lms_link_for_item(location, preview=False): location = Location(location) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 5495b5ea0b..61cd6cde01 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -55,7 +55,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups -from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState +from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item from xmodule.templates import all_templates from xmodule.modulestore.xml_importer import import_from_xml @@ -68,6 +68,9 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] +# cdodge: these are categories which should not be parented, they are detached from the hierarchy +DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] + def _modulestore(location): """ @@ -616,6 +619,17 @@ def save_item(request): # commit to datastore store.update_metadata(item_location, existing_item.metadata) + # cdodge: special case logic for updating static_tabs + # unfortunately tabs are enumerated in the course policy data structure, so if we change the display name of + # the tab, we need to update the course policy (which has a nice .tabs property on it) + item_loc = Location(item_location) + if item_loc.category == 'static_tab': + # VS[compat] Rework when we can stop having to support tabs lists in the policy + tag_module = store.get_item(item_location) + course = get_course_for_item(item_location) + course.update_tab_reference(tag_module) + modulestore('direct').update_metadata(course.location, course.metadata) + return HttpResponse() @@ -689,7 +703,16 @@ def clone_item(request): new_item.metadata['display_name'] = display_name _modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata) - _modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + + if new_item.location.category not in DETACHED_CATEGORIES: + _modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + elif new_item.location.category == 'static_tab': + # static tabs - in our data model - are described in the course policy + # VS[compat]: Rework when we can stop having to support tabs lists in the policy + if parent.location.category != 'course': + raise BaseException('adding a new static_tab must be on a course object') + parent.add_tab_reference(new_item) + _modulestore(parent.location).update_metadata(parent.location.url(), parent.metadata) return HttpResponse(json.dumps({'id': dest_location.url()})) @@ -877,7 +900,7 @@ def edit_tabs(request, org, course, coursename): static_tabs = modulestore('direct').get_items(static_tabs_loc) - # import pudb; pudb.set_trace() + logging.debug('tabs in policy = %s', course_item.tabs) components = [ static_tab.location.url() @@ -995,6 +1018,17 @@ def create_new_course(request): # set a default start date to now new_course.metadata['start'] = stringify_time(time.gmtime()) + # set up the default tabs + # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or + # at least a list populated with the minimal times + # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better + # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here + new_course.tabs = [{"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"}] + modulestore('direct').update_metadata(new_course.location.url(), new_course.own_metadata) create_all_course_groups(request.user, new_course.location) diff --git a/cms/static/coffee/src/views/tabs.coffee b/cms/static/coffee/src/views/tabs.coffee index 9797b9495b..34d86a3051 100644 --- a/cms/static/coffee/src/views/tabs.coffee +++ b/cms/static/coffee/src/views/tabs.coffee @@ -15,7 +15,7 @@ class CMS.Views.TabsEdit extends Backbone.View @$('.components').sortable( handle: '.drag-handle' - update: (event, ui) => alert 'got it!' + update: (event, ui) => alert 'not yet implemented!' helper: 'clone' opacity: '0.5' placeholder: 'component-placeholder' diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html index 7c4fc57ec2..94c5e38260 100644 --- a/cms/templates/edit-tabs.html +++ b/cms/templates/edit-tabs.html @@ -17,7 +17,9 @@ <%block name="content">
      -

      Static Tabs

      +
      +

      Static Tabs

      +
      diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 6e3f450324..fe2c6ca87a 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -266,6 +266,33 @@ class CourseDescriptor(SequenceDescriptor): """ return self.metadata.get('tabs') + @tabs.setter + def tabs(self, value): + self.metadata['tabs'] = value + + def add_tab_reference(self, tab_module): + ''' + Since in CMS we can add static tabs via data, the current data model expects that + the list of tabs be encapsulated in the course. + VS[compat] we can rework this to avoid pointers to static tab modules once we fully deprecate filesystem support + ''' + existing_tabs = self.tabs or [] + existing_tabs.append({'type':'static_tab', 'name' : tab_module.metadata.get('display_name'), 'url_slug' : tab_module.location.name}) + self.tabs = existing_tabs + + + def update_tab_reference(self, tab_module): + ''' + Since in CMS we can add static tabs via data, the current data model expects that + the list of tabs be encapsulated in the course. + VS[compat] we can rework this to avoid pointers to static tab modules once we fully deprecate filesystem support + ''' + existing_tabs = self.tabs + for tab in existing_tabs: + if tab.get('url_slug') == tab_module.location.name: + tab['name'] = tab_module.metadata.get('display_name') + self.tabs = existing_tabs + @property def show_calculator(self): return self.metadata.get("show_calculator", None) == "Yes" From 7ca49f3a144c61aa88d13e37b0251beaa5352703 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 8 Nov 2012 10:54:56 -0500 Subject: [PATCH 0383/1010] remove debug log msg --- cms/djangoapps/contentstore/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b17fa090b6..6c02215be1 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -903,8 +903,6 @@ def edit_tabs(request, org, course, coursename): static_tabs = modulestore('direct').get_items(static_tabs_loc) - logging.debug('tabs in policy = %s', course_item.tabs) - components = [ static_tab.location.url() for static_tab From 50a8cf4aea42000fefb1f71910ee70db6922b0ba Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 8 Nov 2012 10:59:09 -0500 Subject: [PATCH 0384/1010] Add test for course creation --- cms/djangoapps/contentstore/tests/tests.py | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index b5dfc3c4fe..c731815b55 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -208,3 +208,61 @@ class EditTestCase(ContentStoreTestCase): def test_edit_unit_full(self): self.check_edit_unit('full') + +@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +class CourseCreationTest(ContentStoreTestCase): + """Test new course creation code""" + + def setUp(self): + uname = 'testuser' + email = 'edit@test.com' + password = 'foo' + + # Create the use so we can log them in. + # Note that we do not actually need to do anything + # for registration if we directly mark them active. + u = User.objects.create_user(uname, email, password) + u.is_active = True + u.save() + + # Flush and initialize the module store + # It needs a course template because it creates a new course + # by cloning from the template. + xmodule.modulestore.django._MODULESTORES = {} + xmodule.modulestore.django.modulestore().collection.drop() + template_item = {'_id': {'tag': 'i4x', 'org': 'edx', 'course': 'templates', + 'category': 'course', 'name': 'Empty', 'revision': None}} + xmodule.modulestore.django.modulestore().collection.insert(template_item) + + self.client = Client() + self.client.login(username=uname, password=password) + + self.post_data = { + 'template': 'i4x://edx/templates/course/Empty', + 'org': 'MITx', + 'number': '999', + 'display_name': 'Robot Super Course', + } + + def test_create_course(self): + resp = self.client.post(reverse('create_new_course'), self.post_data) + self.assertEqual(resp.status_code, 200) + data = parse_json(resp) + self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') + + def test_create_course_duplicate_course(self): + resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.post_data) + data = parse_json(resp) + self.assertEqual(resp.status_code, 200) + self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') + + def test_create_course_duplicate_number(self): + resp = self.client.post(reverse('create_new_course'), self.post_data) + self.post_data['display_name'] = 'Robot Super Course Two' + + resp = self.client.post(reverse('create_new_course'), self.post_data) + data = parse_json(resp) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(data['ErrMsg'], 'There is already a course defined with the same organization and course number.') \ No newline at end of file From c3c6a7af7e635568970456d9a812c1d3dd3f386f Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 8 Nov 2012 13:29:46 -0500 Subject: [PATCH 0385/1010] address feedback from cale --- cms/djangoapps/contentstore/utils.py | 25 -------- cms/djangoapps/contentstore/views.py | 20 +----- common/lib/xmodule/xmodule/course_module.py | 23 ------- .../lib/xmodule/xmodule/modulestore/mongo.py | 64 ++++++++++++++++++- .../xmodule/templates/about/empty.yaml | 2 +- .../xmodule/templates/courseinfo/empty.yaml | 2 +- .../xmodule/templates/statictab/empty.yaml | 2 +- 7 files changed, 67 insertions(+), 71 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 508236a1e9..18afd331d0 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -33,31 +33,6 @@ def get_course_location_for_item(location): return location -def get_course_for_item(location): - ''' - cdodge: for a given Xmodule, return the course that it belongs to - NOTE: This makes a lot of assumptions about the format of the course location - Also we have to assert that this module maps to only one course item - it'll throw an - assert if not - ''' - item_loc = Location(location) - - # @hack! We need to find the course location however, we don't - # know the 'name' parameter in this context, so we have - # to assume there's only one item in this query even though we are not specifying a name - course_search_location = ['i4x', item_loc.org, item_loc.course, 'course', None] - courses = modulestore().get_items(course_search_location) - - # make sure we found exactly one match on this above course search - found_cnt = len(courses) - if found_cnt == 0: - raise BaseException('Could not find course at {0}'.format(course_search_location)) - - if found_cnt > 1: - raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses)) - - return courses[0] - def get_lms_link_for_item(location, preview=False): location = Location(location) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 6c02215be1..f680dd7262 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -55,7 +55,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups -from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item +from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState from xmodule.templates import all_templates from xmodule.modulestore.xml_importer import import_from_xml @@ -622,17 +622,6 @@ def save_item(request): # commit to datastore store.update_metadata(item_location, existing_item.metadata) - # cdodge: special case logic for updating static_tabs - # unfortunately tabs are enumerated in the course policy data structure, so if we change the display name of - # the tab, we need to update the course policy (which has a nice .tabs property on it) - item_loc = Location(item_location) - if item_loc.category == 'static_tab': - # VS[compat] Rework when we can stop having to support tabs lists in the policy - tag_module = store.get_item(item_location) - course = get_course_for_item(item_location) - course.update_tab_reference(tag_module) - modulestore('direct').update_metadata(course.location, course.metadata) - return HttpResponse() @@ -709,13 +698,6 @@ def clone_item(request): if new_item.location.category not in DETACHED_CATEGORIES: _modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) - elif new_item.location.category == 'static_tab': - # static tabs - in our data model - are described in the course policy - # VS[compat]: Rework when we can stop having to support tabs lists in the policy - if parent.location.category != 'course': - raise BaseException('adding a new static_tab must be on a course object') - parent.add_tab_reference(new_item) - _modulestore(parent.location).update_metadata(parent.location.url(), parent.metadata) return HttpResponse(json.dumps({'id': dest_location.url()})) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index fe2c6ca87a..512247a429 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -270,29 +270,6 @@ class CourseDescriptor(SequenceDescriptor): def tabs(self, value): self.metadata['tabs'] = value - def add_tab_reference(self, tab_module): - ''' - Since in CMS we can add static tabs via data, the current data model expects that - the list of tabs be encapsulated in the course. - VS[compat] we can rework this to avoid pointers to static tab modules once we fully deprecate filesystem support - ''' - existing_tabs = self.tabs or [] - existing_tabs.append({'type':'static_tab', 'name' : tab_module.metadata.get('display_name'), 'url_slug' : tab_module.location.name}) - self.tabs = existing_tabs - - - def update_tab_reference(self, tab_module): - ''' - Since in CMS we can add static tabs via data, the current data model expects that - the list of tabs be encapsulated in the course. - VS[compat] we can rework this to avoid pointers to static tab modules once we fully deprecate filesystem support - ''' - existing_tabs = self.tabs - for tab in existing_tabs: - if tab.get('url_slug') == tab_module.location.name: - tab['name'] = tab_module.metadata.get('display_name') - self.tabs = existing_tabs - @property def show_calculator(self): return self.metadata.get("show_calculator", None) == "Yes" diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 550e6570ac..19f506906c 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -276,10 +276,49 @@ class MongoModuleStore(ModuleStoreBase): source_item = self.collection.find_one(location_to_query(source)) source_item['_id'] = Location(location).dict() self.collection.insert(source_item) - return self._load_items([source_item])[0] + item = self._load_items([source_item])[0] + + # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so + # if we add one then we need to also add it to the policy information (i.e. metadata) + # we should remove this once we can break this reference from the course to static tabs + if location.category == 'static_tab': + course = self.get_course_for_item(item.location) + existing_tabs = course.tabs or [] + existing_tabs.append({'type':'static_tab', 'name' : item.metadata.get('display_name'), 'url_slug' : item.location.name}) + course.tabs = existing_tabs + self.update_metadata(course.location, course.metadata) + + return item except pymongo.errors.DuplicateKeyError: raise DuplicateItemError(location) + + def get_course_for_item(self, location): + ''' + VS[compat] + cdodge: for a given Xmodule, return the course that it belongs to + NOTE: This makes a lot of assumptions about the format of the course location + Also we have to assert that this module maps to only one course item - it'll throw an + assert if not + This is only used to support static_tabs as we need to be course module aware + ''' + + # @hack! We need to find the course location however, we don't + # know the 'name' parameter in this context, so we have + # to assume there's only one item in this query even though we are not specifying a name + course_search_location = ['i4x', location.org, location.course, 'course', None] + courses = self.get_items(course_search_location) + + # make sure we found exactly one match on this above course search + found_cnt = len(courses) + if found_cnt == 0: + raise BaseException('Could not find course at {0}'.format(course_search_location)) + + if found_cnt > 1: + raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses)) + + return courses[0] + def _update_single_item(self, location, update): """ Set update on the specified item, and raises ItemNotFoundError @@ -327,6 +366,19 @@ class MongoModuleStore(ModuleStoreBase): location: Something that can be passed to Location metadata: A nested dictionary of module metadata """ + # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so + # if we add one then we need to also add it to the policy information (i.e. metadata) + # we should remove this once we can break this reference from the course to static tabs + loc = Location(location) + if loc.category == 'static_tab': + course = self.get_course_for_item(loc) + existing_tabs = course.tabs or [] + for tab in existing_tabs: + if tab.get('url_slug') == loc.name: + tab['name'] = metadata.get('display_name') + break + course.tabs = existing_tabs + self.update_metadata(course.location, course.metadata) self._update_single_item(location, {'metadata': metadata}) @@ -336,6 +388,16 @@ class MongoModuleStore(ModuleStoreBase): location: Something that can be passed to Location """ + # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so + # if we add one then we need to also add it to the policy information (i.e. metadata) + # we should remove this once we can break this reference from the course to static tabs + if location.category == 'static_tab': + item = self.get_item(location) + course = self.get_course_for_item(item.location) + existing_tabs = course.tabs or [] + course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name] + self.update_metadata(course.location, course.metadata) + self.collection.remove({'_id': Location(location).dict()}) def get_parent_locations(self, location): diff --git a/common/lib/xmodule/xmodule/templates/about/empty.yaml b/common/lib/xmodule/xmodule/templates/about/empty.yaml index 71bcef8914..fa3ed606bd 100644 --- a/common/lib/xmodule/xmodule/templates/about/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/about/empty.yaml @@ -1,5 +1,5 @@ --- metadata: display_name: Empty -data: "This is where you can add additional information about your course." +data: "

      This is where you can add additional information about your course.

      " children: [] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml index 71bcef8914..fa3ed606bd 100644 --- a/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml @@ -1,5 +1,5 @@ --- metadata: display_name: Empty -data: "This is where you can add additional information about your course." +data: "

      This is where you can add additional information about your course.

      " children: [] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml index 31306e2ce6..410e1496c2 100644 --- a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml @@ -1,5 +1,5 @@ --- metadata: display_name: Empty -data: "This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing." +data: "

      This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing.

      " children: [] \ No newline at end of file From 2077f62dd5431d2898e6c6056b5712cecbf68115 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 8 Nov 2012 14:16:51 -0500 Subject: [PATCH 0386/1010] Add test for index page, plus SOUTH migrations for faster test execution. --- cms/djangoapps/contentstore/tests/tests.py | 83 ++++++++++++---------- cms/envs/test.py | 3 + 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index c731815b55..87fc5eac57 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -182,56 +182,31 @@ TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data' TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) -class EditTestCase(ContentStoreTestCase): - """Check that editing functionality works on example courses""" - - def setUp(self): - email = 'edit@test.com' - password = 'foo' - self.create_account('edittest', email, password) - self.activate_user(email) - self.login(email, password) - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - - def check_edit_unit(self, test_course_name): - import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) - - for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)): - print "Checking ", descriptor.location.url() - print descriptor.__class__, descriptor.location - resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) - self.assertEqual(resp.status_code, 200) - - def test_edit_unit_toy(self): - self.check_edit_unit('toy') - - def test_edit_unit_full(self): - self.check_edit_unit('full') - -@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) -class CourseCreationTest(ContentStoreTestCase): - """Test new course creation code""" +class ContentStoreTest(TestCase): def setUp(self): uname = 'testuser' - email = 'edit@test.com' + email = 'test+courses@edx.org' password = 'foo' # Create the use so we can log them in. # Note that we do not actually need to do anything # for registration if we directly mark them active. - u = User.objects.create_user(uname, email, password) - u.is_active = True - u.save() + self.user = User.objects.create_user(uname, email, password) + self.user.is_active = True + self.user.save() # Flush and initialize the module store # It needs a course template because it creates a new course # by cloning from the template. xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django.modulestore().collection.drop() - template_item = {'_id': {'tag': 'i4x', 'org': 'edx', 'course': 'templates', - 'category': 'course', 'name': 'Empty', 'revision': None}} + template_item = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", + "category" : "course", "name" : "Empty", "revision" : None }, + "definition" : { "children" : [ ], "data" : { "textbooks" : [ ], + "wiki_slug" : None } }, "metadata" : { "start" : "2020-10-10T10:00", + "display_name" : "Empty" } } + xmodule.modulestore.django.modulestore().collection.insert(template_item) self.client = Client() @@ -245,12 +220,14 @@ class CourseCreationTest(ContentStoreTestCase): } def test_create_course(self): + """Test new course creation - happy path""" resp = self.client.post(reverse('create_new_course'), self.post_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') def test_create_course_duplicate_course(self): + """Test new course creation - error path""" resp = self.client.post(reverse('create_new_course'), self.post_data) resp = self.client.post(reverse('create_new_course'), self.post_data) data = parse_json(resp) @@ -258,6 +235,7 @@ class CourseCreationTest(ContentStoreTestCase): self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') def test_create_course_duplicate_number(self): + """Test new course creation - error path""" resp = self.client.post(reverse('create_new_course'), self.post_data) self.post_data['display_name'] = 'Robot Super Course Two' @@ -265,4 +243,35 @@ class CourseCreationTest(ContentStoreTestCase): data = parse_json(resp) self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], 'There is already a course defined with the same organization and course number.') \ No newline at end of file + self.assertEqual(data['ErrMsg'], 'There is already a course defined with the same organization and course number.') + + def test_index(self): + """Test viewing the existing courses""" + # Staff has access to view all courses + self.user.is_staff = True + self.user.save() + + # Create a course so there is something to view + resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.get(reverse('index')) + + # Right now there may be a bug in cms/templates/widgets/header.html + # because there is an unexpected ending ul tag in the header. + # When this is fixed, change html=True below. JZ 11/08/2012 + self.assertContains(resp, 'Robot Super Course', html=False) + + def check_edit_unit(self, test_course_name): + """Check that editing functionality works on example courses""" + import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) + + for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)): + print "Checking ", descriptor.location.url() + print descriptor.__class__, descriptor.location + resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) + self.assertEqual(resp.status_code, 200) + + def test_edit_unit_toy(self): + self.check_edit_unit('toy') + + def test_edit_unit_full(self): + self.check_edit_unit('full') \ No newline at end of file diff --git a/cms/envs/test.py b/cms/envs/test.py index 217ed0e573..235993f5ac 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -21,6 +21,9 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_ROOT = path('test_root') +# Makes the tests run much faster... +SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead + # Want static files in the same dir for running on jenkins. STATIC_ROOT = TEST_ROOT / "staticfiles" From 981f5cee45507bff72f85215696de74db5937359 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 8 Nov 2012 16:07:17 -0500 Subject: [PATCH 0387/1010] initial buildout of a 'xlint' test to verify legacy coursewar --- .../contentstore/management/commands/xlint.py | 26 +++++++++++ .../xmodule/modulestore/xml_importer.py | 43 +++++++++++++++++-- rakefile | 12 ++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 cms/djangoapps/contentstore/management/commands/xlint.py diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py new file mode 100644 index 0000000000..355b639f2d --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, CommandError +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.django import contentstore + + +unnamed_modules = 0 + + +class Command(BaseCommand): + help = \ +'''Verify the structure of courseware as to it's suitability for import''' + + def handle(self, *args, **options): + if len(args) == 0: + raise CommandError("import requires at least one argument: [...]") + + data_dir = args[0] + if len(args) > 1: + course_dirs = args[1:] + else: + course_dirs = None + print "Importing. Data_dir={data}, course_dirs={courses}".format( + data=data_dir, + courses=course_dirs) + import_from_xml(None, data_dir, course_dirs, load_error_modules=False, validate_only=True) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 00ddb6a948..4a3526b53e 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -88,7 +88,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor', - load_error_modules=True, static_content_store=None, target_location_namespace = None): + load_error_modules=True, static_content_store=None, target_location_namespace=None, validate_only=False): """ Import the specified xml data_dir into the "store" modulestore, using org and course as the location org and course. @@ -110,6 +110,10 @@ def import_from_xml(store, data_dir, course_dirs=None, load_error_modules=load_error_modules ) + if validate_only: + validate_module_structure(module_store) + return module_store, [] + # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means # to enumerate the entire collection of course modules. It will be left as a TBD to implement that # method on XmlModuleStore. @@ -192,7 +196,6 @@ def import_from_xml(store, data_dir, course_dirs=None, store.update_item(module.location, module_data) - if 'children' in module.definition: store.update_children(module.location, module.definition['children']) @@ -200,6 +203,38 @@ def import_from_xml(store, data_dir, course_dirs=None, # inherited metadata everywhere. store.update_metadata(module.location, dict(module.own_metadata)) - - return module_store, course_items + + +def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category): + err_cnt = 0 + + parents = [] + # get all modules of parent_category + for module in module_store.modules[course_id].itervalues(): + if module.location.category == parent_category: + parents.append(module) + + for parent in parents: + for child_loc in [Location(child) for child in parent.definition.get('children', [])]: + if child_loc.category != expected_child_category: + err_cnt += 1 + print 'ERROR: child {0} of parent {1} was expected to be category of {2} but was {3}'.format( + child_loc, parent.location, expected_child_category, child_loc.category) + + return err_cnt + +def validate_module_structure(module_store): + err_cnt = 0 + warn_cnt = 0 + for course_id in module_store.modules.keys(): + # constrain that courses only have 'chapter' children + err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter") + # constrain that chapters only have 'sequentials' + err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential") + # constrain that sequentials only have 'verticals' + err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical") + + print "SUMMARY: {0} Errors {1} Warnings".format(err_cnt, warn_cnt) + + diff --git a/rakefile b/rakefile index 4f1c15321f..beb787c8c3 100644 --- a/rakefile +++ b/rakefile @@ -364,6 +364,18 @@ namespace :cms do end end +namespace :cms do + desc "Import course data within the given DATA_DIR variable" + task :xlint do + if ENV['DATA_DIR'] + sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'])) + else + raise "Please specify a DATA_DIR variable that point to your data directory.\n" + + "Example: \`rake cms:import DATA_DIR=../data\`" + end + end +end + desc "Build a properties file used to trigger autodeploy builds" task :autodeploy_properties do File.open("autodeploy.properties", "w") do |file| From ddfbf3b678d7cbde5fa9fbc480b9fc21a428b206 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 8 Nov 2012 17:16:25 -0500 Subject: [PATCH 0388/1010] Add test for course overview page --- cms/djangoapps/contentstore/tests/tests.py | 42 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 87fc5eac57..902c061648 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -190,12 +190,16 @@ class ContentStoreTest(TestCase): password = 'foo' # Create the use so we can log them in. + self.user = User.objects.create_user(uname, email, password) + # Note that we do not actually need to do anything # for registration if we directly mark them active. - self.user = User.objects.create_user(uname, email, password) self.user.is_active = True + # Staff has access to view all courses + self.user.is_staff = True self.user.save() + # Flush and initialize the module store # It needs a course template because it creates a new course # by cloning from the template. @@ -245,12 +249,20 @@ class ContentStoreTest(TestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(data['ErrMsg'], 'There is already a course defined with the same organization and course number.') - def test_index(self): - """Test viewing the existing courses""" - # Staff has access to view all courses - self.user.is_staff = True - self.user.save() + def test_course_index_view_with_no_courses(self): + """Test viewing the index page with no courses""" + # Create a course so there is something to view + resp = self.client.get(reverse('index')) + # Right now there may be a bug in cms/templates/widgets/header.html + # because there is an unexpected ending ul tag in the header. + # When this is fixed, make a better matcher below. JZ 11/08/2012 + self.assertContains(resp, + ' New Course', + html=False) + + def test_course_index_view_with_course(self): + """Test viewing the index page with an existing course""" # Create a course so there is something to view resp = self.client.post(reverse('create_new_course'), self.post_data) resp = self.client.get(reverse('index')) @@ -260,6 +272,24 @@ class ContentStoreTest(TestCase): # When this is fixed, change html=True below. JZ 11/08/2012 self.assertContains(resp, 'Robot Super Course', html=False) + def test_course_overview_view_with_course(self): + """Test viewing the course overview page with an existing course""" + # Create a course so there is something to view + resp = self.client.post(reverse('create_new_course'), self.post_data) + + data = { + 'org': 'MITx', + 'course': '999', + 'name': Location.clean('Robot Super Course'),} + + resp = self.client.get(reverse('course_index', kwargs=data)) + # Right now there may be a bug in cms/templates/widgets/header.html + # because there is an unexpected ending ul tag in the header. + # When this is fixed, change html=True below. JZ 11/08/2012 + self.assertContains(resp, + 'Robot Super Course', + html=False) + def check_edit_unit(self, test_course_name): """Check that editing functionality works on example courses""" import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) From 8f2ab52215a5d9e113b66669c28907d1b28fa66a Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 8 Nov 2012 17:48:38 -0500 Subject: [PATCH 0389/1010] in progress form styling of course info --- cms/static/sass/_settings.scss | 95 +++++++++++++++++++++++++++++++++- cms/templates/settings.html | 20 ++++--- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index e26bf8dd8d..a3d74a5bd0 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -23,6 +23,7 @@ label { display: inline-block; + vertical-align: top; width: 200px; font-size: 15px; font-weight: 400; @@ -37,6 +38,12 @@ } } + .label-micro { + display: block; + margin-top: 5px; + font-size: 13px; + } + input, textarea { font-size: 15px; @@ -55,6 +62,61 @@ &.date { width: 140px; } + + &:focus { + border-color: $blue; + outline: 0; + } + } + + .field { + background: #b6eab1; + display: inline-block; + vertical-align: top; + min-width: 400px; + + input { + display: block; + } + + .input-list { + + .element { + position: relative; + width: 100%; + @include clearfix(); + + div { + float: left; + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } + + .delete-icon { + position: absolute; + right: 10px; + top: 10px; + } + } + + &.element-stacked { + + } + + &.element-multi { + + } + } + + .new-item { + margin-top: 20px; + padding-bottom: 10px; + @include grey-button; + @include box-sizing(border-box); + } } .settings-page-section { @@ -82,6 +144,13 @@ &:last-child { margin-bottom: 0; } + + .tip { + color: $mediumGrey; + display: inline-block; + margin-left: 10px; + font-size: 13px; + } } &:last-child { @@ -89,12 +158,34 @@ } > section { - padding-bottom: 40px; + padding-bottom: 60px; margin-bottom: 40px; border-radius: 3px; - border-bottom: 1px solid $mediumGrey; + border-bottom: 1px solid $lightGrey; @include clearfix; + header { + @include clearfix; + border-bottom: 1px solid $mediumGrey; + margin-bottom: 20px; + padding-bottom: 10px; + + h3 { + color: $darkGrey; + float: left; + + margin: 0 40px 0 0; + text-transform: uppercase; + } + + .detail { + float: right; + marign-top: 3px; + color: $mediumGrey; + font-size: 13px; + } + } + &:last-child { padding-bottom: 0; border-bottom: none; diff --git a/cms/templates/settings.html b/cms/templates/settings.html index fa5443a0c0..e13741ec4c 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -150,23 +150,22 @@
        -
      1. +
      2. Milestone Date
        - + Milestone Name
        -
      - New Class Milestone + New Class Milestone
      @@ -213,7 +212,7 @@ - New Link + New Link
      @@ -233,7 +232,7 @@
        -
      1. +
      2. Textbook Name @@ -244,12 +243,12 @@ Textbook URL
        - +
      - New Textbook + New Textbook
      @@ -266,7 +265,7 @@
        -
      1. +
      2. Question @@ -277,12 +276,11 @@ Answer
        -
      - New Question & Answer + New Question & Answer
    From f1e1f9e7e5b6f943b86036da31773faa610e0d3d Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 8 Nov 2012 18:11:48 -0500 Subject: [PATCH 0390/1010] in progress field layout styling --- cms/static/sass/_settings.scss | 51 ++++++++++++++++++++++------------ cms/templates/settings.html | 18 ++++++------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index a3d74a5bd0..4d292c8f31 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -64,16 +64,16 @@ } &:focus { + @include linear-gradient(tint($blue, 80%), tint($blue, 90%)); border-color: $blue; outline: 0; } } .field { - background: #b6eab1; display: inline-block; vertical-align: top; - min-width: 400px; + max-width: 400px; input { display: block; @@ -87,12 +87,7 @@ @include clearfix(); div { - float: left; - margin-right: 20px; - &:last-child { - margin-right: 0; - } } .delete-icon { @@ -102,12 +97,27 @@ } } - &.element-stacked { + .element-stacked { + div { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } } - &.element-multi { + .element-multi { + div { + float: left; + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } } } @@ -137,19 +147,29 @@ } .row { - margin-bottom: 20px; - padding: 0; - border-bottom: none; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid $lightGrey; &:last-child { margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; } .tip { color: $mediumGrey; + font-size: 13px; + } + + .tip-inline { display: inline-block; margin-left: 10px; - font-size: 13px; + } + + .tip-stacked { + display: block; + margin: 10px 0 0 200px; } } @@ -158,10 +178,7 @@ } > section { - padding-bottom: 60px; - margin-bottom: 40px; - border-radius: 3px; - border-bottom: 1px solid $lightGrey; + margin-bottom: 100px; @include clearfix; header { diff --git a/cms/templates/settings.html b/cms/templates/settings.html index e13741ec4c..4586fc423c 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -92,7 +92,7 @@
  5. Course Details
  6. Staff
  7. Grading
  8. -
  9. Handouts & Guides
  10. +
  11. Handouts
  12. Problems
  13. @@ -119,7 +119,7 @@
    - e.g. 101x + e.g. 101x
    @@ -136,13 +136,13 @@
    - First day the class begins + First day the class begins
    - Last day of class activty + Last day of class activty
    @@ -174,7 +174,7 @@

    Introducing Your Course

    - How your course will be shown to students considering taking it + Information for perspective students
    @@ -185,7 +185,7 @@
    - Used to introduce your class to perspective students + Used to introduce your class to perspective students
    @@ -243,7 +243,7 @@ Textbook URL
    - +
@@ -261,11 +261,11 @@
- +
    -
  1. +
  2. Question From b1451127408c6bab206ef23e14bb3573524cf122 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 9 Nov 2012 08:39:54 -0500 Subject: [PATCH 0391/1010] multi-input field markup and styling --- cms/static/sass/_settings.scss | 27 +++++++++++++++++++++++---- cms/templates/settings.html | 10 ++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index 4d292c8f31..b0f45b2197 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -90,10 +90,24 @@ } - .delete-icon { - position: absolute; - right: 10px; - top: 10px; + .remove-item { + display: block; + border-top: 1px solid $lightGrey; + margin-top: 5px; + padding-top: 10px; + font-size: 13px; + } + } + + .element-group { + width: 400px; + padding: 15px 20px; + background: tint($lightGrey, 50%); + @include border-radius(3px); + @include box-sizing(border-box); + + input.long, textarea { + width: 100%; } } @@ -118,6 +132,11 @@ margin-right: 0; } } + + .remove-item { + float: left; + width: 100%; + } } } diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 4586fc423c..fe3c59f10f 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -150,7 +150,7 @@
      -
    1. +
    2. Milestone Date @@ -161,6 +161,7 @@ Milestone Name
      + Delete Milestone
    @@ -232,7 +233,7 @@
      -
    1. +
    2. Textbook Name @@ -243,7 +244,7 @@ Textbook URL
      - + Delete Textbook
    @@ -265,7 +266,7 @@
      -
    1. +
    2. Question @@ -276,6 +277,7 @@ Answer
      + Delete Question & Answer
    From 5a02e37cce4f380a726d62a191612be8803442bf Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 9 Nov 2012 09:11:56 -0500 Subject: [PATCH 0392/1010] make sure we strip away the leading '/' from the subpath for static content otherwise the naming expectations will break --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 00ddb6a948..7ecfe61814 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -16,7 +16,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ remap_dict = {} # now import all static assets - static_dir = course_data_path / 'static' + static_dir = course_data_path / 'static/' for dirname, dirnames, filenames in os.walk(static_dir): for filename in filenames: From 01889df686f4ac4a74731a4bba1ac7791ac4f8ed Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 9 Nov 2012 09:23:48 -0500 Subject: [PATCH 0393/1010] need to pass in course namespace when rewrite static links in the CMS --- cms/djangoapps/contentstore/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index f680dd7262..c8f8e8152d 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -503,7 +503,8 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ) module.get_html = replace_static_urls( module.get_html, - module.metadata.get('data_dir', module.location.course) + module.metadata.get('data_dir', module.location.course), + course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]) ) save_preview_state(request, preview_id, descriptor.location.url(), module.get_instance_state(), module.get_shared_state()) From 39fda1ea9bb04b60daa6fc1e31508a1268d925ea Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 9 Nov 2012 10:31:08 -0500 Subject: [PATCH 0394/1010] in progress staff section styling --- cms/static/sass/_settings.scss | 37 ++++++++++++++++++++++-- cms/templates/settings.html | 51 ++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index b0f45b2197..c2e285ce4a 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -70,6 +70,15 @@ } } + ::-webkit-input-placeholder { + color: $mediumGrey; + font-size: 13px; + } + :-moz-placeholder { + color: $mediumGrey; + font-size: 13px; + } + .field { display: inline-block; vertical-align: top; @@ -93,7 +102,7 @@ .remove-item { display: block; border-top: 1px solid $lightGrey; - margin-top: 5px; + margin-top: 10px; padding-top: 10px; font-size: 13px; } @@ -101,7 +110,7 @@ .element-group { width: 400px; - padding: 15px 20px; + padding: 15px; background: tint($lightGrey, 50%); @include border-radius(3px); @include box-sizing(border-box); @@ -242,6 +251,30 @@ } } } + + .settings-details { + + } + + .settings-staff { + + } + + .settings-grading { + + } + + .settings-handouts { + + } + + .settings-problems { + + } + + .settings-discussions { + + } } h2 { diff --git a/cms/templates/settings.html b/cms/templates/settings.html index fe3c59f10f..77800f99c5 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -94,6 +94,7 @@
  3. Grading
  4. Handouts
  5. Problems
  6. +
  7. Discussions
@@ -152,7 +153,7 @@
  1. - + Milestone Date
    @@ -210,7 +211,16 @@ + +
    +

    Discussions

    + +
From 7efaa87c19c15b61711053e6378d0e324738c946 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 9 Nov 2012 10:54:50 -0500 Subject: [PATCH 0395/1010] Add test for clone item. Also change password hashing for faster tests. --- cms/djangoapps/contentstore/tests/tests.py | 71 +++++++++++++++------- cms/envs/test.py | 7 +++ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 902c061648..b82b88d29c 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -201,53 +201,69 @@ class ContentStoreTest(TestCase): # Flush and initialize the module store - # It needs a course template because it creates a new course + # It needs the templates because it creates new records # by cloning from the template. xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django.modulestore().collection.drop() - template_item = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", - "category" : "course", "name" : "Empty", "revision" : None }, - "definition" : { "children" : [ ], "data" : { "textbooks" : [ ], - "wiki_slug" : None } }, "metadata" : { "start" : "2020-10-10T10:00", - "display_name" : "Empty" } } - - xmodule.modulestore.django.modulestore().collection.insert(template_item) + course_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", + "category" : "course", "name" : "Empty", "revision" : None }, + "definition" : { "children" : [ ], "data" : { "textbooks" : [ ], + "wiki_slug" : None } }, + "metadata" : { "start" : "2020-10-10T10:00", "display_name" : "Empty" } } + section_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", + "category" : "section", "name" : "Empty", "revision" : None }, + "definition" : { "children" : [ ], "data" : "" }, + "metadata" : { "display_name" : "Empty" } } + chapter_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", + "category" : "chapter", "name" : "Empty", "revision" : None }, + "definition" : { "children" : [ ], "data" : "" }, + "metadata" : { "display_name" : "Empty" } } + xmodule.modulestore.django.modulestore().collection.insert(course_template) + xmodule.modulestore.django.modulestore().collection.insert(section_template) + xmodule.modulestore.django.modulestore().collection.insert(chapter_template) self.client = Client() self.client.login(username=uname, password=password) - self.post_data = { + self.course_data = { 'template': 'i4x://edx/templates/course/Empty', 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', - } + } + + self.section_data = { + 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', + 'template' : 'i4x://edx/templates/chapter/Empty', + 'display_name': 'Section One', + } def test_create_course(self): """Test new course creation - happy path""" - resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') def test_create_course_duplicate_course(self): """Test new course creation - error path""" - resp = self.client.post(reverse('create_new_course'), self.post_data) - resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) data = parse_json(resp) self.assertEqual(resp.status_code, 200) self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') def test_create_course_duplicate_number(self): """Test new course creation - error path""" - resp = self.client.post(reverse('create_new_course'), self.post_data) - self.post_data['display_name'] = 'Robot Super Course Two' + resp = self.client.post(reverse('create_new_course'), self.course_data) + self.course_data['display_name'] = 'Robot Super Course Two' - resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) data = parse_json(resp) self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], 'There is already a course defined with the same organization and course number.') + self.assertEqual(data['ErrMsg'], + 'There is already a course defined with the same organization and course number.') def test_course_index_view_with_no_courses(self): """Test viewing the index page with no courses""" @@ -264,7 +280,7 @@ class ContentStoreTest(TestCase): def test_course_index_view_with_course(self): """Test viewing the index page with an existing course""" # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) resp = self.client.get(reverse('index')) # Right now there may be a bug in cms/templates/widgets/header.html @@ -275,12 +291,13 @@ class ContentStoreTest(TestCase): def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.post_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) data = { - 'org': 'MITx', - 'course': '999', - 'name': Location.clean('Robot Super Course'),} + 'org': 'MITx', + 'course': '999', + 'name': Location.clean('Robot Super Course'), + } resp = self.client.get(reverse('course_index', kwargs=data)) # Right now there may be a bug in cms/templates/widgets/header.html @@ -290,6 +307,16 @@ class ContentStoreTest(TestCase): 'Robot Super Course', html=False) + def test_clone_item(self): + """Test cloning an item. E.g. creating a new section""" + resp = self.client.post(reverse('create_new_course'), self.course_data) + resp = self.client.post(reverse('clone_item'), self.section_data) + + self.assertEqual(resp.status_code, 200) + data = parse_json(resp) + self.assertRegexpMatches(data['id'], + '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$') + def check_edit_unit(self, test_course_name): """Check that editing functionality works on example courses""" import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) diff --git a/cms/envs/test.py b/cms/envs/test.py index 0f69ab682e..cb84f32ff1 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -99,3 +99,10 @@ CACHES = { 'KEY_FUNCTION': 'util.memcache.safe_key', } } + +################### Make tests faster +#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/ +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', +) \ No newline at end of file From f249c227a5ac2b42b264dfe42d1bfaa33393d5e8 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 9 Nov 2012 11:05:37 -0500 Subject: [PATCH 0396/1010] Fix typo in close tag in header --- cms/templates/widgets/header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 0f5780a5d2..877f03533c 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -24,6 +24,6 @@ % else: Log in % endif - + From 58eea1bc8088c74727bfe5fb1ac4afe3c52d800c Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 9 Nov 2012 11:40:23 -0500 Subject: [PATCH 0397/1010] Remove section close tag that had no open tag. --- cms/templates/index.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/cms/templates/index.html b/cms/templates/index.html index e195daaed0..652acfa0ea 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -61,7 +61,4 @@ - - - From bc34d79dbf4f84b054b9088cbec9c46c6de17450 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 9 Nov 2012 12:14:11 -0500 Subject: [PATCH 0398/1010] Improve assertions by using HTML matchers. --- cms/djangoapps/contentstore/tests/tests.py | 39 +++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index b82b88d29c..832feae8be 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -199,7 +199,6 @@ class ContentStoreTest(TestCase): self.user.is_staff = True self.user.save() - # Flush and initialize the module store # It needs the templates because it creates new records # by cloning from the template. @@ -238,6 +237,17 @@ class ContentStoreTest(TestCase): 'display_name': 'Section One', } + def tearDown(self): + # Make sure you flush out the test modulestore after the end + # of the last test otherwise cms/djangoapps/contentstore/__init__.py + # update_templates() is complaining that there are duplicate + # templates. + # If your test module gets in some weird state, do this manually + # from the bash shell to drop it. + # $ mongo test_xmodule --eval "db.dropDatabase()" + xmodule.modulestore.django._MODULESTORES = {} + xmodule.modulestore.django.modulestore().collection.drop() + def test_create_course(self): """Test new course creation - happy path""" resp = self.client.post(reverse('create_new_course'), self.course_data) @@ -269,24 +279,20 @@ class ContentStoreTest(TestCase): """Test viewing the index page with no courses""" # Create a course so there is something to view resp = self.client.get(reverse('index')) - - # Right now there may be a bug in cms/templates/widgets/header.html - # because there is an unexpected ending ul tag in the header. - # When this is fixed, make a better matcher below. JZ 11/08/2012 self.assertContains(resp, - ' New Course', - html=False) + '

My Courses

', + status_code=200, + html=True) def test_course_index_view_with_course(self): """Test viewing the index page with an existing course""" # Create a course so there is something to view resp = self.client.post(reverse('create_new_course'), self.course_data) resp = self.client.get(reverse('index')) - - # Right now there may be a bug in cms/templates/widgets/header.html - # because there is an unexpected ending ul tag in the header. - # When this is fixed, change html=True below. JZ 11/08/2012 - self.assertContains(resp, 'Robot Super Course', html=False) + self.assertContains(resp, + 'Robot Super Course', + status_code=200, + html=True) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" @@ -300,12 +306,10 @@ class ContentStoreTest(TestCase): } resp = self.client.get(reverse('course_index', kwargs=data)) - # Right now there may be a bug in cms/templates/widgets/header.html - # because there is an unexpected ending ul tag in the header. - # When this is fixed, change html=True below. JZ 11/08/2012 self.assertContains(resp, 'Robot Super Course', - html=False) + status_code=200, + html=True) def test_clone_item(self): """Test cloning an item. E.g. creating a new section""" @@ -327,6 +331,9 @@ class ContentStoreTest(TestCase): resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) self.assertEqual(resp.status_code, 200) + xmodule.modulestore.django._MODULESTORES = {} + xmodule.modulestore.django.modulestore().collection.drop() + def test_edit_unit_toy(self): self.check_edit_unit('toy') From da3c3e5f20589ea8f8d16471f2cc358c5d6c3d78 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 9 Nov 2012 13:54:12 -0500 Subject: [PATCH 0399/1010] initial xlint implementation. Accumulate all import errors during XmlModuleStore importing. Also do checks post XmlModuleStore import and assert that the structure (course->chapter->sequential->vertical) is present in the courses. --- .../contentstore/management/commands/xlint.py | 6 ++-- common/lib/xmodule/xmodule/modulestore/xml.py | 7 ++-- .../xmodule/modulestore/xml_importer.py | 32 ++++++++++++++++++- common/lib/xmodule/xmodule/seq_module.py | 4 ++- rakefile | 4 ++- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 355b639f2d..e8f7b248e4 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -9,8 +9,10 @@ unnamed_modules = 0 class Command(BaseCommand): help = \ -'''Verify the structure of courseware as to it's suitability for import''' - + ''' + Verify the structure of courseware as to it's suitability for import + To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] + ''' def handle(self, *args, **options): if len(args) == 0: raise CommandError("import requires at least one argument: [...]") diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 6794703998..64ccf73d5e 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -303,7 +303,7 @@ class XMLModuleStore(ModuleStoreBase): try: course_descriptor = self.load_course(course_dir, errorlog.tracker) except Exception as e: - msg = "Failed to load course '{0}': {1}".format(course_dir, str(e)) + msg = "ERROR: Failed to load course '{0}': {1}".format(course_dir, str(e)) log.exception(msg) errorlog.tracker(msg) @@ -337,7 +337,7 @@ class XMLModuleStore(ModuleStoreBase): with open(policy_path) as f: return json.load(f) except (IOError, ValueError) as err: - msg = "Error loading course policy from {0}".format(policy_path) + msg = "ERROR: loading course policy from {0}".format(policy_path) tracker(msg) log.warning(msg + " " + str(err)) return {} @@ -458,7 +458,8 @@ class XMLModuleStore(ModuleStoreBase): module.metadata['data_dir'] = course_dir self.modules[course_descriptor.id][module.location] = module except Exception, e: - logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e))) + logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e))) + system.error_tracker("ERROR: " + str(e)) def get_instance(self, course_id, location, depth=0): """ diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index a1739851ac..23eea58a97 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -227,6 +227,28 @@ def validate_category_hierarcy(module_store, course_id, parent_category, expecte def validate_module_structure(module_store): err_cnt = 0 warn_cnt = 0 + + print module_store.errored_courses + + # first count all errors and warnings as part of the XMLModuleStore import + for err_log in module_store._location_errors.itervalues(): + for err_log_entry in err_log.errors: + msg = err_log_entry[0] + if msg.startswith('ERROR:'): + err_cnt+=1 + else: + warn_cnt+=1 + + # then count outright all courses that failed to load at all + for err_log in module_store.errored_courses.itervalues(): + for err_log_entry in err_log.errors: + msg = err_log_entry[0] + print msg + if msg.startswith('ERROR:'): + err_cnt+=1 + else: + warn_cnt+=1 + for course_id in module_store.modules.keys(): # constrain that courses only have 'chapter' children err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter") @@ -235,6 +257,14 @@ def validate_module_structure(module_store): # constrain that sequentials only have 'verticals' err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical") - print "SUMMARY: {0} Errors {1} Warnings".format(err_cnt, warn_cnt) + print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt) + + if err_cnt > 0: + print "This course is not suitable for importing. Please fix courseware according to specifications before importing." + elif warn_cnt > 0: + print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing" + else: + print "This course can be imported successfully." + diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 0ade3e0e7d..155ad99480 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): for child in xml_object: try: children.append(system.process_xml(etree.tostring(child)).location.url()) - except: + except Exception, e: log.exception("Unable to load child when parsing Sequence. Continuing...") + if system.error_tracker is not None: + system.error_tracker("ERROR: " + str(e)) continue return {'children': children} diff --git a/rakefile b/rakefile index beb787c8c3..d8386fbda2 100644 --- a/rakefile +++ b/rakefile @@ -367,7 +367,9 @@ end namespace :cms do desc "Import course data within the given DATA_DIR variable" task :xlint do - if ENV['DATA_DIR'] + if ENV['DATA_DIR'] and ENV['COURSE_DIR'] + sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR'])) + elsif ENV['DATA_DIR'] sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'])) else raise "Please specify a DATA_DIR variable that point to your data directory.\n" + From 5f926ebad07390caa0f007322ad357a208efe644 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 9 Nov 2012 14:34:04 -0500 Subject: [PATCH 0400/1010] Add better comments and modulestore cleanup. --- cms/djangoapps/contentstore/tests/tests.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 832feae8be..34b47503d1 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -239,9 +239,10 @@ class ContentStoreTest(TestCase): def tearDown(self): # Make sure you flush out the test modulestore after the end - # of the last test otherwise cms/djangoapps/contentstore/__init__.py - # update_templates() is complaining that there are duplicate - # templates. + # of the last test because otherwise on the next run + # cms/djangoapps/contentstore/__init__.py + # update_templates() will try to update the templates + # via upsert and it seems to be messing things up. # If your test module gets in some weird state, do this manually # from the bash shell to drop it. # $ mongo test_xmodule --eval "db.dropDatabase()" @@ -322,7 +323,6 @@ class ContentStoreTest(TestCase): '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$') def check_edit_unit(self, test_course_name): - """Check that editing functionality works on example courses""" import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)): @@ -331,11 +331,9 @@ class ContentStoreTest(TestCase): resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) self.assertEqual(resp.status_code, 200) - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - def test_edit_unit_toy(self): self.check_edit_unit('toy') def test_edit_unit_full(self): - self.check_edit_unit('full') \ No newline at end of file + self.check_edit_unit('full') + From a060b8de9c6791f7471efde171f507c9ed3e0d63 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 13 Nov 2012 08:35:22 -0500 Subject: [PATCH 0401/1010] Use xmodule.templates instead of hard coding them in the test --- cms/djangoapps/contentstore/tests/tests.py | 26 +++++----------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 34b47503d1..b0bb6c86a1 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -202,24 +202,13 @@ class ContentStoreTest(TestCase): # Flush and initialize the module store # It needs the templates because it creates new records # by cloning from the template. + # Note that if your test module gets in some weird state + # (though it shouldn't), do this manually + # from the bash shell to drop it: + # $ mongo test_xmodule --eval "db.dropDatabase()" xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django.modulestore().collection.drop() - course_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", - "category" : "course", "name" : "Empty", "revision" : None }, - "definition" : { "children" : [ ], "data" : { "textbooks" : [ ], - "wiki_slug" : None } }, - "metadata" : { "start" : "2020-10-10T10:00", "display_name" : "Empty" } } - section_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", - "category" : "section", "name" : "Empty", "revision" : None }, - "definition" : { "children" : [ ], "data" : "" }, - "metadata" : { "display_name" : "Empty" } } - chapter_template = { "_id" : { "tag" : "i4x", "org" : "edx", "course" : "templates", - "category" : "chapter", "name" : "Empty", "revision" : None }, - "definition" : { "children" : [ ], "data" : "" }, - "metadata" : { "display_name" : "Empty" } } - xmodule.modulestore.django.modulestore().collection.insert(course_template) - xmodule.modulestore.django.modulestore().collection.insert(section_template) - xmodule.modulestore.django.modulestore().collection.insert(chapter_template) + xmodule.templates.update_templates() self.client = Client() self.client.login(username=uname, password=password) @@ -242,10 +231,7 @@ class ContentStoreTest(TestCase): # of the last test because otherwise on the next run # cms/djangoapps/contentstore/__init__.py # update_templates() will try to update the templates - # via upsert and it seems to be messing things up. - # If your test module gets in some weird state, do this manually - # from the bash shell to drop it. - # $ mongo test_xmodule --eval "db.dropDatabase()" + # via upsert and it sometimes seems to be messing things up. xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django.modulestore().collection.drop() From 3c90e2927a916085239dea80d56a1d5fcdc4cdf2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 13 Nov 2012 10:35:54 -0500 Subject: [PATCH 0402/1010] Create the test_root/log directory on git checkout --- test_root/log/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test_root/log/.gitkeep diff --git a/test_root/log/.gitkeep b/test_root/log/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From 630c2fa21ed53b8af1bb9b41bb9bd4597e4f7d95 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 13 Nov 2012 10:55:17 -0500 Subject: [PATCH 0403/1010] Make jasmine testing quieter --- cms/envs/jasmine.py | 4 +++- common/lib/logsettings.py | 11 ++++++++--- lms/envs/jasmine.py | 4 +++- rakefile | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index b29e170411..5c9be1cf9c 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, - debug=True) + debug=True, + local_loglevel='ERROR', + console_loglevel='ERROR') PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ diff --git a/common/lib/logsettings.py b/common/lib/logsettings.py index 2b001b0517..1e96534128 100644 --- a/common/lib/logsettings.py +++ b/common/lib/logsettings.py @@ -3,6 +3,7 @@ import platform import sys from logging.handlers import SysLogHandler +LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] def get_logger_config(log_dir, logging_env="no_env", @@ -11,7 +12,8 @@ def get_logger_config(log_dir, dev_env=False, syslog_addr=None, debug=False, - local_loglevel='INFO'): + local_loglevel='INFO', + console_loglevel=None): """ @@ -30,9 +32,12 @@ def get_logger_config(log_dir, """ # Revert to INFO if an invalid string is passed in - if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: + if local_loglevel not in LOG_LEVELS: local_loglevel = 'INFO' + if console_loglevel is None or console_loglevel not in LOG_LEVELS: + console_loglevel = 'DEBUG' if debug else 'INFO' + hostname = platform.node().split(".")[0] syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s " "[{hostname} %(process)d] [%(filename)s:%(lineno)d] " @@ -55,7 +60,7 @@ def get_logger_config(log_dir, }, 'handlers': { 'console': { - 'level': 'DEBUG' if debug else 'INFO', + 'level': console_loglevel, 'class': 'logging.StreamHandler', 'formatter': 'standard', 'stream': sys.stdout, diff --git a/lms/envs/jasmine.py b/lms/envs/jasmine.py index 317628f8ba..8551d80504 100644 --- a/lms/envs/jasmine.py +++ b/lms/envs/jasmine.py @@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, - debug=True) + debug=True, + local_loglevel='ERROR', + console_loglevel='ERROR') PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ diff --git a/rakefile b/rakefile index 4f1c15321f..5156c78147 100644 --- a/rakefile +++ b/rakefile @@ -47,7 +47,7 @@ def django_for_jasmine(system, django_reload) end django_pid = fork do - exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' ')) + exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' ')) end jasmine_url = 'http://localhost:12345/_jasmine/' up = false From b779a421d719f20e49e3c10cdca4483174b9a412 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 14 Nov 2012 12:37:17 -0500 Subject: [PATCH 0404/1010] check for the existence of static and static/subs directories --- .../xmodule/modulestore/xml_importer.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 23eea58a97..c6764b491c 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -111,7 +111,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ) if validate_only: - validate_module_structure(module_store) + perform_xlint(data_dir, course_dirs, module_store) return module_store, [] # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means @@ -224,11 +224,34 @@ def validate_category_hierarcy(module_store, course_id, parent_category, expecte return err_cnt -def validate_module_structure(module_store): +def validate_data_source_path_existence(path, is_err = True, extra_msg = None): + _cnt = 0 + if not os.path.exists(path): + print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if + extra_msg is not None else '')) + _cnt = 1 + return _cnt + +def validate_data_source_paths(data_dir, course_dir): + # check that there is a '/static/' directory + course_path = data_dir / course_dir + err_cnt = 0 + warn_cnt = 0 + err_cnt += validate_data_source_path_existence(course_path / 'static') + warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False, + extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.') + return err_cnt, warn_cnt + + +def perform_xlint(data_dir, course_dirs,module_store): err_cnt = 0 warn_cnt = 0 - print module_store.errored_courses + # check all data source path information + for course_dir in course_dirs: + _err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir) + err_cnt += _err_cnt + warn_cnt += _warn_cnt # first count all errors and warnings as part of the XMLModuleStore import for err_log in module_store._location_errors.itervalues(): From 5c3db6f50250986fdf99925e752c02f7e408d255 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 14 Nov 2012 13:35:54 -0500 Subject: [PATCH 0405/1010] Change the Embed column in assest index page to just display the URL --- cms/templates/asset_index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 83e2188691..900ef3b697 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -28,7 +28,7 @@ {{uploadDate}} - + @@ -47,7 +47,7 @@ Name Date Added - Embed + URL @@ -68,7 +68,7 @@ ${asset['uploadDate']} - '> + % endfor From 65869461f819e3bcd3dad5908e40cd3d6b522056 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Wed, 14 Nov 2012 14:50:48 -0500 Subject: [PATCH 0406/1010] finalized layout and styling for course details and faculty settings --- cms/static/sass/_settings.scss | 765 +++++++++++++++++++-------------- cms/templates/settings.html | 619 +++++++++++++++----------- 2 files changed, 809 insertions(+), 575 deletions(-) diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss index c2e285ce4a..40d991937d 100644 --- a/cms/static/sass/_settings.scss +++ b/cms/static/sass/_settings.scss @@ -5,12 +5,13 @@ display: table; width: 100%; + // layout .sidebar { display: table-cell; float: none; width: 20%; padding: 30px 0 30px 20px; - border-radius: 3px 0 0 3px; + @include border-radius(3px 0 0 3px); background: $lightGrey; } @@ -21,139 +22,16 @@ padding: 30px 40px 30px 60px; } - label { - display: inline-block; - vertical-align: top; - width: 200px; - font-size: 15px; - font-weight: 400; - - &.check-label { - display: inline; - margin-left: 10px; - } - - &.ranges { - margin-bottom: 20px; - } - } - - .label-micro { - display: block; - margin-top: 5px; - font-size: 13px; - } - - input, textarea { - font-size: 15px; - - &.long { - width: 400px; - } - - &.tall { - height: 200px; - } - - &.short { - width: 100px; - } - - &.date { - width: 140px; - } - - &:focus { - @include linear-gradient(tint($blue, 80%), tint($blue, 90%)); - border-color: $blue; - outline: 0; - } - } - - ::-webkit-input-placeholder { - color: $mediumGrey; - font-size: 13px; - } - :-moz-placeholder { - color: $mediumGrey; - font-size: 13px; - } - - .field { - display: inline-block; - vertical-align: top; - max-width: 400px; - - input { + .settings-page-menu { + a { display: block; - } + padding-left: 20px; + line-height: 52px; - .input-list { - - .element { - position: relative; - width: 100%; - @include clearfix(); - - div { - - } - - .remove-item { - display: block; - border-top: 1px solid $lightGrey; - margin-top: 10px; - padding-top: 10px; - font-size: 13px; - } + &.is-shown { + background: #fff; + @include border-radius(5px 0 0 5px); } - - .element-group { - width: 400px; - padding: 15px; - background: tint($lightGrey, 50%); - @include border-radius(3px); - @include box-sizing(border-box); - - input.long, textarea { - width: 100%; - } - } - - .element-stacked { - - div { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - } - - .element-multi { - - div { - float: left; - margin-right: 20px; - - &:last-child { - margin-right: 0; - } - } - - .remove-item { - float: left; - width: 100%; - } - } - } - - .new-item { - margin-top: 20px; - padding-bottom: 10px; - @include grey-button; - @include box-sizing(border-box); } } @@ -174,37 +52,17 @@ display: block; } - .row { - margin-bottom: 15px; - padding-bottom: 15px; - border-bottom: 1px solid $lightGrey; - - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; - } - - .tip { - color: $mediumGrey; - font-size: 13px; - } - - .tip-inline { - display: inline-block; - margin-left: 10px; - } - - .tip-stacked { - display: block; - margin: 10px 0 0 200px; - } - } - &:last-child { border-bottom: none; } + > .title { + margin-bottom: 30px; + font-size: 28px; + font-weight: 300; + color: $blue; + } + > section { margin-bottom: 100px; @include clearfix; @@ -239,25 +97,276 @@ } } - .settings-page-menu { - a { - display: block; - padding-left: 20px; - line-height: 52px; + // form basics + label, .label { + padding: 0; + border: none; + background: none; + font-size: 15px; + font-weight: 400; - &.is-shown { - background: #fff; - border-radius: 5px 0 0 5px; + &.check-label { + display: inline; + margin-left: 10px; + } + + &.ranges { + margin-bottom: 20px; + } + } + + input, textarea { + @include transition(all 1s ease-in-out); + @include box-sizing(border-box); + font-size: 15px; + + &.long { + width: 100%; + } + + &.tall { + height: 200px; + } + + &.short { + width: 25%; + } + + &.date { + + } + + &:focus { + @include linear-gradient(tint($blue, 80%), tint($blue, 90%)); + border-color: $blue; + outline: 0; + } + } + + ::-webkit-input-placeholder { + color: $mediumGrey; + font-size: 13px; + } + :-moz-placeholder { + color: $mediumGrey; + font-size: 13px; + } + + .tip { + color: $mediumGrey; + font-size: 13px; + } + + + // form layouts + .row { + margin-bottom: 30px; + padding-bottom: 30px; + border-bottom: 1px solid $lightGrey; + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } + + // structural labels, not semantic labels per se + > label, .label { + display: inline-block; + vertical-align: top; + width: 200px; + } + + // tips + .tip-inline { + display: inline-block; + margin-left: 10px; + } + + .tip-stacked { + display: block; + margin-top: 10px; + } + + // structural field, not semantic fields per se + .field { + display: inline-block; + width: 400px; + + > input, > textarea, .input { + display: inline-block; + + &:last-child { + margin-bottom: 0; + } + + .group { + input, textarea { + margin-bottom: 5px; + } + + .label, label { + font-size: 13px; + } + } + + // multi-field + &.multi { + display: block; + background: tint($lightGrey, 50%); + padding: 15px; + @include border-radius(4px); + @include box-sizing(border-box); + + .group { + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + input, .input, textarea { + + } + } + } + + // multi stacked + &.multi-stacked { + + .group { + input, .input, textarea { + width: 100%; + } + } + } + + // multi-field inline + &.multi-inline { + @include clearfix; + + .group { + float: left; + margin-right: 20px; + width: 170px; + + &:nth-child(2) { + margin-right: 0; + } + + .input, input, textarea { + width: 100%; + } + } + + .remove-item { + float: right; + } + } + } + + // input-list + .input-list { + + .input { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px dotted $lightGrey; + + &:last-child { + border: 0; + } + } + } + + // enumerated inputs + &.enum { } } } + // editing controls - adding + .new-item, .replace-item { + clear: both; + display: block; + margin-top: 10px; + padding-bottom: 10px; + @include grey-button; + @include box-sizing(border-box); + } + + + // editing controls - removing + .remove-item { + clear: both; + display: block; + opacity: 0.75; + font-size: 13px; + text-align: right; + @include transition(opacity 0.25s ease-in-out); + + + &:hover { + color: $blue; + opacity: 0.99; + } + } + + // editing controls - preview + .input-existing { + display: block !important; + + .current { + width: 100%; + margin: 10px 0; + padding: 15px; + @include box-sizing(border-box); + @include border-radius(5px); + background: tint($blue, 80%); + } + } + + + // specific sections .settings-details { } - .settings-staff { + .settings-faculty { + .settings-faculty-members { + + > header { + display: none; + } + + .field .multi { + display: block; + margin-bottom: 40px; + padding: 20px; + background: tint($lightGrey, 50%); + @include border-radius(4px); + @include box-sizing(border-box); + } + + .course-faculty-list-item { + + .row { + + &:nth-child(4) { + padding-bottom: 0; + border-bottom: none; + } + } + } + + #course-faculty-bio-input { + margin-bottom: 0; + } + + .new-course-faculty-item { + } + } } .settings-grading { @@ -275,194 +384,200 @@ .settings-discussions { } - } - h2 { - margin-bottom: 30px; - font-size: 28px; - font-weight: 300; - color: $blue; - } + // states + label.is-focused { + color: $blue; + @include transition(color 1s ease-in-out); + } - h3 { - margin-bottom: 30px; - font-size: 15px; - font-weight: 700; - color: $blue; - } - - .grade-controls { - @include clearfix; - } - - .new-grade-button { - position: relative; - float: left; - display: block; - width: 29px; - height: 29px; - margin: 4px 10px 0 0; - border-radius: 20px; - border: 1px solid $darkGrey; - @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); - background-color: #d1dae3; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); - color: #6d788b; - - .plus-icon { - position: absolute; - top: 50%; - left: 50%; - margin-left: -6px; - margin-top: -6px; + // misc + .divide { + display: none; } } - .grade-slider { - float: left; - width: 560px; - height: 60px; - .grade-bar { - position: relative; - width: 100%; - height: 40px; - background: $lightGrey; - .increments { - position: relative; + // h3 { + // margin-bottom: 30px; + // font-size: 15px; + // font-weight: 700; + // color: $blue; + // } - li { - position: absolute; - top: 42px; - width: 30px; - margin-left: -15px; - font-size: 9px; - text-align: center; + // .grade-controls { + // @include clearfix; + // } - &.increment-0 { - left: 0; - } + // .new-grade-button { + // position: relative; + // float: left; + // display: block; + // width: 29px; + // height: 29px; + // margin: 4px 10px 0 0; + // border-radius: 20px; + // border: 1px solid $darkGrey; + // @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); + // background-color: #d1dae3; + // @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + // color: #6d788b; - &.increment-10 { - left: 10%; - } + // .plus-icon { + // position: absolute; + // top: 50%; + // left: 50%; + // margin-left: -6px; + // margin-top: -6px; + // } + // } - &.increment-20 { - left: 20%; - } + // .grade-slider { + // float: left; + // width: 560px; + // height: 60px; - &.increment-30 { - left: 30%; - } + // .grade-bar { + // position: relative; + // width: 100%; + // height: 40px; + // background: $lightGrey; - &.increment-40 { - left: 40%; - } + // .increments { + // position: relative; - &.increment-50 { - left: 50%; - } + // li { + // position: absolute; + // top: 42px; + // width: 30px; + // margin-left: -15px; + // font-size: 9px; + // text-align: center; - &.increment-60 { - left: 60%; - } + // &.increment-0 { + // left: 0; + // } - &.increment-70 { - left: 70%; - } + // &.increment-10 { + // left: 10%; + // } - &.increment-80 { - left: 80%; - } + // &.increment-20 { + // left: 20%; + // } - &.increment-90 { - left: 90%; - } + // &.increment-30 { + // left: 30%; + // } - &.increment-100 { - left: 100%; - } - } - } + // &.increment-40 { + // left: 40%; + // } - .grades { - position: relative; + // &.increment-50 { + // left: 50%; + // } - li { - position: absolute; - top: 0; - height: 40px; - text-align: right; + // &.increment-60 { + // left: 60%; + // } - &:hover, - &.is-dragging { - .remove-button { - display: block; - } - } + // &.increment-70 { + // left: 70%; + // } - .remove-button { - display: none; - position: absolute; - top: -17px; - right: 1px; - height: 17px; - font-size: 10px; - } + // &.increment-80 { + // left: 80%; + // } - &:nth-child(1) { - background: #4fe696; - } + // &.increment-90 { + // left: 90%; + // } - &:nth-child(2) { - background: #ffdf7e; - } + // &.increment-100 { + // left: 100%; + // } + // } + // } - &:nth-child(3) { - background: #ffb657; - } + // .grades { + // position: relative; - &:nth-child(4) { - background: #fb336c; - } + // li { + // position: absolute; + // top: 0; + // height: 40px; + // text-align: right; - &:nth-child(5) { - background: #ef54a1; - } + // &:hover, + // &.is-dragging { + // .remove-button { + // display: block; + // } + // } - .letter-grade { - display: block; - margin: 7px 5px 0 0; - font-size: 14px; - font-weight: 700; - line-height: 14px; - } + // .remove-button { + // display: none; + // position: absolute; + // top: -17px; + // right: 1px; + // height: 17px; + // font-size: 10px; + // } - .range { - display: block; - margin-right: 5px; - font-size: 9px; - line-height: 12px; - } + // &:nth-child(1) { + // background: #4fe696; + // } - .drag-bar { - position: absolute; - top: 0; - right: -1px; - height: 40px; - width: 2px; - background-color: #fff; - cursor: ew-resize; - @include transition(none); + // &:nth-child(2) { + // background: #ffdf7e; + // } - &:hover { - width: 4px; - right: -2px; - } - } - } - } - } - } + // &:nth-child(3) { + // background: #ffb657; + // } + + // &:nth-child(4) { + // background: #fb336c; + // } + + // &:nth-child(5) { + // background: #ef54a1; + // } + + // .letter-grade { + // display: block; + // margin: 7px 5px 0 0; + // font-size: 14px; + // font-weight: 700; + // line-height: 14px; + // } + + // .range { + // display: block; + // margin-right: 5px; + // font-size: 9px; + // line-height: 12px; + // } + + // .drag-bar { + // position: absolute; + // top: 0; + // right: -1px; + // height: 40px; + // width: 2px; + // background-color: #fff; + // cursor: ew-resize; + // @include transition(none); + + // &:hover { + // width: 4px; + // right: -2px; + // } + // } + // } + // } + // } + // } } \ No newline at end of file diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 77800f99c5..b7753674bf 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -15,6 +15,12 @@ var gradeThresholds; var GRADES = ['A', 'B', 'C', 'D', 'E']; + $(" :input, textarea").focus(function() { + $("label[for='" + this.id + "']").addClass("is-focused"); + }).blur(function() { + $("label").removeClass("is-focused"); + }); + (function() { $body = $('body'); $gradeBar = $('.grade-bar'); @@ -82,6 +88,7 @@ <%block name="content"> +

Settings

@@ -90,9 +97,8 @@
-
-

Course Details

+
+

Course Details

Basic Information

- The nuts and bolts of your class + The nuts and bolts of your course
- - -
-
- - -
-
- - - e.g. 101x -
-
- - -
-
- -
-
-

Dates & Times

- The nuts and bolts of your class -
- -
- - - First day the class begins -
- -
- - - Last day of class activty -
- -
- - +
-
    -
  1. -
    - - Milestone Date + +
    +
+ +
+ +
+ +
+
+ +
+ +
+
+ + e.g. 101x +
+
+
+
+ +
+ +
+
+

Course Schedule

+ Important steps and segments of your your course +
+ +
+ +
+
+ + First day the class begins +
+
+
+ +
+ +
+
+ + Last day the class begins +
+
+
+ +
+

Milestones:

+ +
+
    +
  • +
    + +
    -
    - - Milestone Name +
    + +
    Delete Milestone
  • - + +
  • +
    + + +
    + +
    + + +
    +
  • +
- New Class Milestone + New Course Milestone
-
-
+
-
+
+ +
+
+
+
+ + + Replace Syllabus + + PDF formatting preferred +
+ +
+ + Upload Syllabus + + PDF formatting preferred +
+
+
+
+ +
+ +

Introducing Your Course

Information for perspective students
- - -
+ +
+ + Detailed summary of concepts and lessons covered +
+
- - - Used to introduce your class to perspective students -
+ +
+ + 1-2 sentences used to introduce your class to perspective students +
+
- - -
- + +
+
+
+ +
+ + + Replace Video + + Video restrictions go here +
+ +
+ + Upload Video + + Video restrictions go here +
+
+ + + +
@@ -203,68 +286,101 @@
- - -
- -
- - +
- -
    -
  1. -
    - -
    - - Delete Link -
  2. -
- - - New Link - + + Supplies, software, and set-up that students will need
-
- -
- -
- - -
+ +
+ + Time students should spend on all course work +
+
- +

Textbooks:

-
-
    -
  1. -
    - - Textbook Name +
    +
+ +
  • +
    + + +
    - +
    + + +
    + +
  • + + + New Textbook
    -
    + + +
    +

    Prerequisites:

    + +
    +
      +
    • +
      + + +
      + +
      + + +
      + + Delete Prerequisite +
    • + +
    • +
      + + +
      + +
      + + +
      +
    • +
    + + + New Prerequisite + +
    +
    +
    +

    More Information

    @@ -272,24 +388,38 @@
    - +

    FAQs:

    -
    -
      -
    1. -
      - - Question +
      +
    + +
  • +
    + + +
    + +
    + + +
    + + Delete Question & Answer +
  • + New Question & Answer @@ -297,149 +427,138 @@
    - - + -
    -

    Course Staff

    +
    +

    Faculty

    -
    +
    -

    Faculty

    +

    Faculty Members

    + Individuals instructing and help with this course
    -
      -
    1. -
      - - -
      -
      - - -
      -
      - - - This photo will appear on your course's info page -
      -
      - - - A brief description of your education, experience, and expertise -
      -
    2. -
    +
    + + + + New Faculty Member + +
    -
    + +
    -

    Grading

    -
    - -
    - -
    -
    -
      -
    1. 0
    2. -
    3. 10
    4. -
    5. 20
    6. -
    7. 30
    8. -
    9. 40
    10. -
    11. 50
    12. -
    13. 60
    14. -
    15. 70
    16. -
    17. 80
    18. -
    19. 90
    20. -
    21. 100
    22. -
    -
      -
    1. - A - 81-100 - remove -
    2. -
    3. - B - 71-80 - - remove -
    4. -
    5. - C - 0-70 - - remove -
    6. -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -

    Homework

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

    Lab

    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    +

    Grading

    -
    -

    Handouts & Guides

    - -
    - - - PDF formatted file -
    -
    +
    -

    Problems

    -
    - -
    -
    +

    Problems

    + +
    -

    Discussions

    +

    Discussions

    -
    + From 58fef4a32a7c83fda74895448177ea78de6c5c1c Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 14 Nov 2012 14:52:24 -0500 Subject: [PATCH 0407/1010] Create factory for courses --- .../contentstore/tests/factories.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 cms/djangoapps/contentstore/tests/factories.py diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py new file mode 100644 index 0000000000..bfb7e7fc20 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -0,0 +1,57 @@ +from factory import Factory +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from time import gmtime +from uuid import uuid4 +from xmodule.timeparse import stringify_time + + +class XModuleCourseFactory(Factory): + """ + Factory for XModule courses. + """ + + ABSTRACT_FACTORY = True + + @classmethod + def _create(cls, target_class, *args, **kwargs): + + template = Location('i4x', 'edx', 'templates', 'course', 'Empty') + org = kwargs.get('org') + number = kwargs.get('number') + display_name = kwargs.get('display_name') + location = Location('i4x', org, number, + 'course', Location.clean(display_name)) + + store = modulestore('direct') + + # Write the data to the mongo datastore + new_course = store.clone_item(template, location) + + # This metadata code was copied from cms/djangoapps/contentstore/views.py + if display_name is not None: + new_course.metadata['display_name'] = display_name + + new_course.metadata['data_dir'] = uuid4().hex + new_course.metadata['start'] = stringify_time(gmtime()) + new_course.tabs = [{"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"}] + + # Update the data in the mongo datastore + store.update_metadata(new_course.location.url(), new_course.own_metadata) + + return new_course + +class Course: + pass + +class CourseFactory(XModuleCourseFactory): + FACTORY_FOR = Course + + template = 'i4x://edx/templates/course/Empty' + org = 'MITx' + number = '999' + display_name = 'Robot Super Course' \ No newline at end of file From 93cc17cf3be9b4eba0432e2c2ff2b32251d38862 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 14 Nov 2012 15:03:55 -0500 Subject: [PATCH 0408/1010] Starting to generate jasmine tests for common/lib --- common/lib/.gitignore | 1 + .../lib/xmodule/jasmine_test_runner.html.erb | 44 ++++++++++++ .../xmodule/js/spec/capa/display_spec.coffee | 1 + .../xmodule/xmodule/js}/src/xmodule.coffee | 0 rakefile | 70 ++++++++++++++----- 5 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 common/lib/.gitignore create mode 100644 common/lib/xmodule/jasmine_test_runner.html.erb rename common/{static/coffee => lib/xmodule/xmodule/js}/src/xmodule.coffee (100%) diff --git a/common/lib/.gitignore b/common/lib/.gitignore new file mode 100644 index 0000000000..bf6b783416 --- /dev/null +++ b/common/lib/.gitignore @@ -0,0 +1 @@ +*/jasmine_test_runner.html diff --git a/common/lib/xmodule/jasmine_test_runner.html.erb b/common/lib/xmodule/jasmine_test_runner.html.erb new file mode 100644 index 0000000000..01d55e50a9 --- /dev/null +++ b/common/lib/xmodule/jasmine_test_runner.html.erb @@ -0,0 +1,44 @@ + + + + Jasmine Test Runner + + + + + + + + + + + + + + + <% for src in js_source %> + + <% end %> + + + <% for src in js_specs %> + + <% end %> + + + + + + + + diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 107930c3b1..9910e8969d 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -8,6 +8,7 @@ describe 'Problem', -> MathJax.Hub.getAllJax.andReturn [@stubbedJax] window.update_schematics = -> + jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures' loadFixtures 'problem.html' spyOn Logger, 'log' spyOn($.fn, 'load').andCallFake (url, callback) -> diff --git a/common/static/coffee/src/xmodule.coffee b/common/lib/xmodule/xmodule/js/src/xmodule.coffee similarity index 100% rename from common/static/coffee/src/xmodule.coffee rename to common/lib/xmodule/xmodule/js/src/xmodule.coffee diff --git a/rakefile b/rakefile index 4f1c15321f..a9bcdd8d79 100644 --- a/rakefile +++ b/rakefile @@ -3,6 +3,8 @@ require 'tempfile' require 'net/http' require 'launchy' require 'colorize' +require 'erb' +require 'tempfile' # Build Constants REPO_ROOT = File.dirname(__FILE__) @@ -79,6 +81,25 @@ def django_for_jasmine(system, django_reload) end end +def template_jasmine_runner(lib) + coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"] + if !coffee_files.empty? + sh("coffee -c #{coffee_files.join(' ')}") + end + phantom_jasmine_path = File.expand_path("common/test/phantom-jasmine") + common_js_root = File.expand_path("common/static/js") + common_coffee_root = File.expand_path("common/static/coffee/src") + js_specs = Dir["#{lib}/**/js/spec/**/*.js"].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} + js_source = Dir["#{lib}/**/*.js"].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} - js_specs + template = ERB.new(File.read("#{lib}/jasmine_test_runner.html.erb")) + template_output = "#{lib}/jasmine_test_runner.html" + File.open(template_output, 'w') do |f| + f.write(template.result(binding)) + end + yield File.expand_path(template_output) +end + + def report_dir_path(dir) return File.join(REPORT_DIR, dir.to_s) end @@ -126,22 +147,6 @@ end end task :pylint => "pylint_#{system}" - desc "Open jasmine tests in your default browser" - task "browse_jasmine_#{system}" do - django_for_jasmine(system, true) do |jasmine_url| - Launchy.open(jasmine_url) - puts "Press ENTER to terminate".red - $stdin.gets - end - end - - desc "Use phantomjs to run jasmine tests from the console" - task "phantomjs_jasmine_#{system}" do - phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' - django_for_jasmine(system, false) do |jasmine_url| - sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}") - end - end end $failed_tests = 0 @@ -210,6 +215,23 @@ TEST_TASK_DIRS = [] end end end + + desc "Open jasmine tests for #{system} in your default browser" + task "browse_jasmine_#{system}" do + django_for_jasmine(system, true) do |jasmine_url| + Launchy.open(jasmine_url) + puts "Press ENTER to terminate".red + $stdin.gets + end + end + + desc "Use phantomjs to run jasmine tests for #{system} from the console" + task "phantomjs_jasmine_#{system}" do + phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' + django_for_jasmine(system, false) do |jasmine_url| + sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}") + end + end end desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" @@ -245,6 +267,22 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| sh("nosetests #{lib}") end + desc "Open jasmine tests for #{lib} in your default browser" + task "browse_jasmine_#{lib}" do + template_jasmine_runner(lib) do |f| + sh("python -m webbrowser -t 'file://#{f}'") + puts "Press ENTER to terminate".red + $stdin.gets + end + end + + desc "Use phantomjs to run jasmine tests for #{lib} from the console" + task "phantomjs_jasmine_#{lib}" do + phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' + template_jasmine_runner(lib) do |f| + sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{f}") + end + end end task :report_dirs From 6f5b3fa1bb86fa08f20ce74f0beca4da1d4b5b03 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 14 Nov 2012 15:12:09 -0500 Subject: [PATCH 0409/1010] Create factory for Xmodule items --- .../contentstore/tests/factories.py | 49 ++++++++++++++++++- cms/djangoapps/contentstore/tests/tests.py | 13 +++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py index bfb7e7fc20..624b792096 100644 --- a/cms/djangoapps/contentstore/tests/factories.py +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -54,4 +54,51 @@ class CourseFactory(XModuleCourseFactory): template = 'i4x://edx/templates/course/Empty' org = 'MITx' number = '999' - display_name = 'Robot Super Course' \ No newline at end of file + display_name = 'Robot Super Course' + +class XModuleItemFactory(Factory): + """ + Factory for XModule items. + """ + + ABSTRACT_FACTORY = True + + @classmethod + def _create(cls, target_class, *args, **kwargs): + + DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] + + parent_location = Location(kwargs.get('parent_location')) + template = Location(kwargs.get('template')) + display_name = kwargs.get('display_name') + + store = modulestore('direct') + + parent = store.get_item(parent_location) + dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + new_item = store.clone_item(template, dest_location) + + # TODO: This needs to be deleted when we have proper storage for static content + new_item.metadata['data_dir'] = parent.metadata['data_dir'] + + # replace the display name with an optional parameter passed in from the caller + if display_name is not None: + new_item.metadata['display_name'] = display_name + + store.update_metadata(new_item.location.url(), new_item.own_metadata) + + if new_item.location.category not in DETACHED_CATEGORIES: + store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + + return new_item + +class Item: + pass + +class ItemFactory(XModuleItemFactory): + FACTORY_FOR = Item + + parent_location = 'i4x://MITx/999/course/Robot_Super_Course' + template = 'i4x://edx/templates/chapter/Empty' + display_name = 'Section One' \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index b0bb6c86a1..72909cbdf1 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -323,3 +323,16 @@ class ContentStoreTest(TestCase): def test_edit_unit_full(self): self.check_edit_unit('full') + def test_factory(self): + + from factories import * + + course = CourseFactory.create() + print '\n' + print course + print '\n' + section = ItemFactory.create() + + print '\n' + print section + print '\n' From 0654665b20023ba19171ef26ef3de564e5ac2cac Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 15 Nov 2012 11:40:29 -0500 Subject: [PATCH 0410/1010] Enable XModule factories to work with the currently version of Factory Boy at PyPI (1.2.0) so that the code will work with 'pip install factory_boy' --- .../contentstore/tests/factories.py | 9 ++++ cms/djangoapps/contentstore/tests/tests.py | 47 +++++++++---------- test-requirements.txt | 1 + 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py index 624b792096..3274477098 100644 --- a/cms/djangoapps/contentstore/tests/factories.py +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -6,12 +6,19 @@ from uuid import uuid4 from xmodule.timeparse import stringify_time +def XMODULE_COURSE_CREATION(class_to_create, **kwargs): + return XModuleCourseFactory._create(class_to_create, **kwargs) + +def XMODULE_ITEM_CREATION(class_to_create, **kwargs): + return XModuleItemFactory._create(class_to_create, **kwargs) + class XModuleCourseFactory(Factory): """ Factory for XModule courses. """ ABSTRACT_FACTORY = True + _creation_function = (XMODULE_COURSE_CREATION,) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -62,6 +69,7 @@ class XModuleItemFactory(Factory): """ ABSTRACT_FACTORY = True + _creation_function = (XMODULE_ITEM_CREATION,) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -74,6 +82,7 @@ class XModuleItemFactory(Factory): store = modulestore('direct') + # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) dest_location = parent_location._replace(category=template.category, name=uuid4().hex) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 72909cbdf1..9ddbe049ad 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -14,6 +14,7 @@ import xmodule.modulestore.django from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml import copy +from factories import * def parse_json(response): @@ -220,12 +221,6 @@ class ContentStoreTest(TestCase): 'display_name': 'Robot Super Course', } - self.section_data = { - 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', - 'template' : 'i4x://edx/templates/chapter/Empty', - 'display_name': 'Section One', - } - def tearDown(self): # Make sure you flush out the test modulestore after the end # of the last test because otherwise on the next run @@ -271,20 +266,27 @@ class ContentStoreTest(TestCase): status_code=200, html=True) + def test_course_factory(self): + course = CourseFactory.create() + self.assertIsInstance(course, xmodule.course_module.CourseDescriptor) + + def test_item_factory(self): + course = CourseFactory.create() + item = ItemFactory.create(parent_location=course.location) + self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor) + def test_course_index_view_with_course(self): """Test viewing the index page with an existing course""" - # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.course_data) + CourseFactory.create(display_name='Robot Super Educational Course') resp = self.client.get(reverse('index')) self.assertContains(resp, - 'Robot Super Course', + 'Robot Super Educational Course', status_code=200, html=True) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" - # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.course_data) + CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') data = { 'org': 'MITx', @@ -300,8 +302,15 @@ class ContentStoreTest(TestCase): def test_clone_item(self): """Test cloning an item. E.g. creating a new section""" - resp = self.client.post(reverse('create_new_course'), self.course_data) - resp = self.client.post(reverse('clone_item'), self.section_data) + CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + + section_data = { + 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', + 'template' : 'i4x://edx/templates/chapter/Empty', + 'display_name': 'Section One', + } + + resp = self.client.post(reverse('clone_item'), section_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) @@ -323,16 +332,4 @@ class ContentStoreTest(TestCase): def test_edit_unit_full(self): self.check_edit_unit('full') - def test_factory(self): - from factories import * - - course = CourseFactory.create() - print '\n' - print course - print '\n' - section = ItemFactory.create() - - print '\n' - print section - print '\n' diff --git a/test-requirements.txt b/test-requirements.txt index c9c15b340d..7048faad38 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,4 @@ coverage nosexcover pylint pep8 +factory_boy From 39c33b42b284a11f6325de6a14e57831d69e18b4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 15 Nov 2012 13:56:42 -0500 Subject: [PATCH 0411/1010] remove 'mode' property when creating a new CodeMirror control. This appears to completely crash if the user puts in a + + + + + diff --git a/lms/templates/course.html b/lms/templates/course.html index 50a00f9d31..a3217d2da5 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -5,6 +5,9 @@ %> <%page args="course" />
    + %if course.metadata.get('is_new'): + New + %endif
    diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 0c45faa923..a8fe851d19 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -20,21 +20,13 @@ ## I'm removing this for now since we aren't using it for the fall. ## <%include file="course_filter.html" />
    -
    - %for course in universities['MITx']: +
      + %for course in courses: +
    • <%include file="../course.html" args="course=course" /> +
    • %endfor -
    -
    - %for course in universities['HarvardX']: - <%include file="../course.html" args="course=course" /> - %endfor -
    -
    - %for course in universities['BerkeleyX']: - <%include file="../course.html" args="course=course" /> - %endfor -
    +
    diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index 833a237251..415199141d 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -6,7 +6,25 @@ ## EdX Blog - 2012-10-14T14:08:12-07:00 + 2012-12-19T14:00:12-07:00 + + tag:www.edx.org,2012:Post/10 + 2012-12-19T14:00:00-07:00 + 2012-12-19T14:00:00-07:00 + + edX announces first wave of new courses for Spring 2013 + <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> + <p></p> + + + tag:www.edx.org,2012:Post/9 + 2012-12-10T14:00:00-07:00 + 2012-12-10T14:00:00-07:00 + + Georgetown University joins edX + <img src="${static.url('images/press/releases/georgetown-seal_240x180.png')}" /> + <p>Sixth institution to join global movement in year one</p> + tag:www.edx.org,2012:Post/8 2012-12-04T14:00:00-07:00 diff --git a/lms/templates/index.html b/lms/templates/index.html index 05fef3dffa..d82c9120d4 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -48,7 +48,7 @@

    Explore free courses from edX universities

    -
      +
      1. @@ -65,7 +65,7 @@
    -
  • +
  • @@ -73,42 +73,46 @@
  • + + +
    + +
    1. - +
      UTx
    2. -
    3. +
    4. - +
      WellesleyX
    5. - +
    6. + + +
      + GeorgetownX +
      +
      +
    -
    - %for course in universities['MITx']: - <%include file="course.html" args="course=course" /> +
      + %for course in courses: +
    • + <%include file="course.html" args="course=course" /> +
    • %endfor -
    -
    - %for course in universities['HarvardX']: - <%include file="course.html" args="course=course" /> - %endfor -
    -
    - %for course in universities['BerkeleyX']: - <%include file="course.html" args="course=course" /> - %endfor -
    +
    diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html new file mode 100644 index 0000000000..33580c6267 --- /dev/null +++ b/lms/templates/instructor/staff_grading.html @@ -0,0 +1,90 @@ +<%inherit file="/main.html" /> +<%block name="bodyclass">${course.css_class} +<%namespace name='static' file='/static_content.html'/> + +<%block name="headextra"> + <%static:css group='course'/> + + +<%block name="title">${course.number} Staff Grading + +<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> + +<%block name="js_extra"> + <%static:js group='staff_grading'/> + + +
    + +
    +

    Staff grading

    + +
    +
    +
    +
    +
    +

    Instructions

    +
    +

    This is the list of problems that current need to be graded in order to train the machine learning models. Each problem needs to be trained separately, and we have indicated the number of student submissions that need to be graded in order for a model to be generated. You can grade more than the minimum required number of submissions--this will improve the accuracy of machine learning, though with diminishing returns. You can see the current accuracy of machine learning while grading.

    +
    + +

    Problem List

    +
      +
    +
    + +
    +

    +
    +

    Problem Information

    +
    +
    +

    Maching Learning Information

    +
    +
    +
    +
    +

    Question

    +
    +
    +
    +
    +

    Grading Rubric

    +
    +
    +
    + +
    + +
    + +
    + +
    +

    Grading

    + +
    +
    +

    Student Submission

    +
    +
    +
    +
    +

    +

    + +
    + + +
    + + +
    + +
    + +
    +
    diff --git a/lms/templates/open_ended_error.html b/lms/templates/open_ended_error.html new file mode 100644 index 0000000000..58a90f86ef --- /dev/null +++ b/lms/templates/open_ended_error.html @@ -0,0 +1,12 @@ +
    +
    +
    + There was an error with your submission. Please contact course staff. +
    +
    +
    +
    + ${errors} +
    +
    +
    \ No newline at end of file diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html new file mode 100644 index 0000000000..cb90006456 --- /dev/null +++ b/lms/templates/open_ended_feedback.html @@ -0,0 +1,16 @@ +
    +
    Feedback
    +
    +
    +

    Score: ${score}

    + % if grader_type == "ML": +

    Check below for full feedback:

    + % endif +
    +
    +
    +
    + ${ feedback | n} +
    +
    +
    \ No newline at end of file diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 88549e9f56..91472cbdaf 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -6,7 +6,7 @@
    - +
    ${initial_rubric}
    diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index acd00bafe8..030eaa5013 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -12,83 +12,71 @@ Press Contact + +

    Organization

    -

    What is edX?

    -

    edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

    -

    EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

    +

    What is edX?

    +

    edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

    +

    EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

    -
    -

    Why is Wellesley College joining edX?

    -

    Wellesley College brings a long history, nearly 150 years, of providing liberal arts courses of the highest quality. WellesleyX courses, and the creativity and innovation of the Wellesley faculty, will provide a new perspective from which the hundreds of thousands of edX learners can benefit.

    -

    Wellesley’s unique, highly personalized, discussion-based learning experience and its commitment to providing pedagogical innovation will mesh with ongoing research into how students learn and how technology can transform learning both on-campus and online.

    -

    As with all consortium members, the values of Wellesley are aligned with those of edX. Wellesley and edX are both committed to expanding access to education to learners of all ages, means, and backgrounds. Both institutions are also committed to the non-profit model.

    -
    -
    -

    Wellesley is the first women’s college to offer courses through a massive open online course (MOOC) platform. What does this mean for the world of online learning?

    -

    Wellesley is currently the only women’s college that has announced plans to offer courses through a massive open online course (MOOC) platform. Wellesley’s commitment to educating women to be leaders in their fields, their communities, and the world provides a unique opportunity for edX learners who come from virtually every nation around the world. Women who have had limited access to education, regardless of where they live, will have access to the best courses, taught by the best faculty, from the best women’s college in the world. The potential for a life-changing educational experience for women has never been as great.

    -
    -
    -

    How many WellesleyX courses will be offered initially? When?

    -

    Initially, WellesleyX will begin offering edX courses in the fall of 2013. The courses, which will offer students the opportunity to explore classic liberal arts and sciences as well as other subjects, will be of the same high quality and rigor as those offered on the Wellesley campus.

    -

    Will edX be adding additional X Universities?

    -

    More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the “X University” Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities”. Each member of the consortium will offer courses on the edX platform as an “X University.” The gathering of many universities’ educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

    -

    edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more “X Universities.”

    +

    More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities". Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

    +

    edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more "X Universities."

    Students

    -

    Who can take edX courses? Will there be an admissions process?

    -

    EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

    +

    Who can take edX courses? Will there be an admissions process?

    +

    EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

    -

    Will certificates be awarded?

    -

    Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

    +

    Will certificates be awarded?

    +

    Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

    -

    What will the scope of the online courses be? How many? Which faculty?

    -

    Our goal is to offer a wide variety of courses across disciplines. There are currently seven courses offered for Fall 2012.

    +

    What will the scope of the online courses be? How many? Which faculty?

    +

    Our goal is to offer a wide variety of courses across disciplines. There are currently nine courses offered for Fall 2012.

    -

    Who is the learner? Domestic or international? Age range?

    -

    Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

    +

    Who is the learner? Domestic or international? Age range?

    +

    Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don't have a target group of potential learners, as the goal is to make these courses available to anyone in the world - from any demographic - who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

    -

    Will participating universities’ standards apply to all courses offered on the edX platform?

    -

    Yes: the reach changes exponentially, but the rigor remains the same.

    +

    Will participating universities' standards apply to all courses offered on the edX platform?

    +

    Yes: the reach changes exponentially, but the rigor remains the same.

    -

    How do you intend to test whether this approach is improving learning?

    -

    Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

    +

    How do you intend to test whether this approach is improving learning?

    +

    Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

    -

    How may I apply to study with edX?

    -

    Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

    +

    How may I apply to study with edX?

    +

    Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

    -

    How may another university participate in edX?

    -

    If you are from a university interested in discussing edX, please email university@edx.org

    +

    How may another university participate in edX?

    +

    If you are from a university interested in discussing edX, please email university@edx.org

    Technology Platform

    -

    What technology will edX use?

    -

    The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

    -

    The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

    +

    What technology will edX use?

    +

    The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

    +

    The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

    -

    How is this different from what other universities are doing online?

    -

    EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

    +

    How is this different from what other universities are doing online?

    +

    EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

    @@ -96,7 +84,6 @@ @@ -104,5 +91,5 @@
    %if user.is_authenticated(): - <%include file="../signup_modal.html" /> +<%include file="../signup_modal.html" /> %endif diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index 15fcbfcdca..d783403970 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -5,7 +5,8 @@

    Do You Want to Change the Future of Education?

    -
    + +
    @@ -27,23 +28,133 @@
    -
    + +
    +
    +
    -

    We're hiring!

    -

    Are you passionate? Want to help change the world? Good, you've found the right company! We're growing and our team needs the best and brightest in creating the next evolution in interactive online education.

    -

    Want to apply to edX?

    -

    Send your resume and cover letter to jobs@edx.org.

    -

    Note: We'll review each and every resume but please note you may not get a response due to the volume of inquiries.

    +

    EdX is looking to add new talent to our team!

    +

    Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status

    +

    Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access free education.  We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.

    +

    Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

    +

    As part of the edX team, you’ll receive:

    +
      +
    • Competitive compensation
    • +
    • Generous benefits package
    • +
    • Free lunch every day
    • +
    • A great working experience where everyone cares
    • +
    +

    While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.

    +
    + +
    +
    +

    INSTRUCTIONAL DESIGNER — CONTRACT OPPORTUNITY

    +

    The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.

    +

    Responsibilities:

    +
      +
    • Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.
    • +
    • Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.
    • +
    • Develop flipped classroom instructional strategies in coordination with community college faculty.
    • +
    • Produce clear and instructionally effective copy, instructional text, and audio and video scripts
    • +
    • Identify and deploy instructional design best practices for edX course staff and faculty as needed.
    • +
    • Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.
    • +
    • Serve as a liaison to instructional design teams based at X universities.
    • +
    • Consult on peer review processes to be used by learners in selected courses.
    • +
    • Ability to apply game-based learning theory and design into selected courses as appropriate.
    • +
    • Use learning analytics and metrics to inform course design and revision process.
    • +
    • Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.
    • +
    • Support the development of pilot courses and modules used for sponsored research initiatives.
    • +
    +

    Qualifications:

    +
      +
    • Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.
    • +
    • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential. Ability to meet deadlines and manage expectations of constituents.
    • +
    • Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
    • +
    • Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
    • +
    +

    Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.

    +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + +
    +
    +

    MEMBER SERVICES MANAGER

    +

    The edX Member Services Manager is responsible for both defining support best practices and directly supporting edX members by handling or routing issues that come in from our websites, email and social media tools.  We are looking for a passionate person to help us define and own this experience. While this is a Manager level position, we see this candidate quickly moving through the ranks, leading a larger team of employees over time. This staff member will be running our fast growth support organization.

    +

    Responsibilities:

    +
      +
    • Define and rollout leading technology, best practices and policies to support a growing team of member care representatives.
    • +
    • Provide reports and visibility into member care metrics.
    • +
    • Identify a staffing plan that mirrors growth and work to grow the team with passionate, member-first focused staff.
    • +
    • Manage member services staff to predefined service levels.
    • +
    • Resolve issues according to edX policies; escalates non-routine issues.
    • +
    • Educate members on edX policies and getting started
    • +
    • May assist new members with edX procedures and processing registration issues.
    • +
    • Provides timely follow-up and resolution to issues.
    • +
    • A passion for doing the right thing - at edX the member is always our top priority
      +
    • +
    +

    Qualifications:

    +
      +
    • 5-8 years in a call center or support team management
    • +
    • Exemplary customer service skills
    • +
    • Experience in creating and rolling out support/service best practices
    • +
    • Solid computer skills – must be fluent with desktop applications and have a basic understanding of web technologies (i.e. basic HTML)
    • +
    • Problem solving - the individual identifies and resolves problems in a timely manner, gathers and analyzes information skillfully and maintains confidentiality.
    • +
    • Interpersonal skills - the individual maintains confidentiality, remains open to others' ideas and exhibits willingness to try new things.
    • +
    • Oral communication - the individual speaks clearly and persuasively in positive or negative situations and demonstrates group presentation skills.
    • +
    • Written communication – the individual edits work for spelling and grammar, presents numerical data effectively and is able to read and interpret written information.
    • +
    • Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
    • +
    • Dependability - the individual is consistently at work and on time, follows instructions, responds to management direction and solicits feedback to improve performance.
    • +
    • College degree
    • +
    +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + +
    +
    +

    DIRECTOR OF PR AND COMMUNICATIONS

    +

    The edX Director of PR & Communications is responsible for creating and executing all PR strategy and providing company-wide leadership to help create and refine the edX core messages and identity as the revolutionary global leader in both on-campus and worldwide education. The Director will design and direct a communications program that conveys cohesive and compelling information about edX's mission, activities, personnel and products while establishing a distinct identity for edX as the leader in online education for both students and learning institutions.

    +

    Responsibilities:

    +
      +
    • Develop and execute goals and strategy for a comprehensive external and internal communications program focused on driving student engagement around courses and institutional adoption of the edX learning platform.
    • +
    • Work with media, either directly or through our agency of record, to establish edX as the industry leader in global learning.
    • +
    • Work with key influencers including government officials on a global scale to ensure the edX mission, content and tools are embraced and supported worldwide.
    • +
    • Work with marketing colleagues to co-develop and/or monitor and evaluate the content and delivery of all communications messages and collateral.
    • +
    • Initiate and/or plan thought leadership events developed to heighten target-audience awareness; participate in meetings and trade shows
    • +
    • Conduct periodic research to determine communications benchmarks
    • +
    • Inform employees about edX's vision, values, policies, and strategies to enable them to perform their jobs efficiently and drive morale.
    • +
    • Work with and manage existing communications team to effectively meet strategic goals.
    • +
    +

    Qualifications:

    +
      +
    • Ten years of experience in PR and communications
    • +
    • Ability to work creatively and provide company-wide leadership in a fast-paced, dynamic start-up environment required
    • +
    • Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
    • +
    • Experience in working in successful consumer-focused startups preferred
    • +
    • PR agency experience in setting strategy for complex multichannel, multinational organizations a plus.
    • +
    • Extensive writing experience and simply amazing oral, written, and interpersonal communications skills
    • +
    • B.A./B.S. in communications or related field
    • +
    +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    +
    +
    - - - - +

    Positions

    +

    How to Apply

    E-mail your resume, coverletter and any other materials to jobs@edx.org

    Our Location

    diff --git a/lms/templates/static_templates/press_releases/Georgetown_joins_edX.html b/lms/templates/static_templates/press_releases/Georgetown_joins_edX.html new file mode 100644 index 0000000000..310a4ced5e --- /dev/null +++ b/lms/templates/static_templates/press_releases/Georgetown_joins_edX.html @@ -0,0 +1,73 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">Georgetown University joins edX +
    + + +
    + +
    +

    Georgetown University joins edX

    +
    +
    +

    Georgetown becomes sixth institution to join global movement in year one, Broadens course options and brings its unique mission-driven perspective to the world of online learning

    + +

    CAMBRIDGE, MA — December 10, 2012 — EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today the addition of Georgetown University to its group of educational leaders who are focused on providing a category-leading, quality higher education experience to the global online community.

    + +

    “It is a privilege to partner with edX and this extraordinary collection of universities,” said Dr. John J. DeGioia, President of Georgetown University. “Our Catholic and Jesuit identity compels us to work at the frontiers of excellence in higher education, and we see in this partnership an exciting opportunity to more fully realize this mission. Not only will it enrich our capacity to serve our global family–beyond our campuses here in Washington, D.C.–but it will also allow us to extend the applications of our research and our scholarship.”

    + +

    Georgetown University, the nation’s oldest Catholic and Jesuit university, is one of the world’s leading academic and research institutions, offering a unique educational experience that prepares the next generation of global citizens to lead and make a difference in the world. Students receive a world-class learning experience focused on educating the whole person through exposure to different faiths, cultures and beliefs. Georgetown University will provide a series of GeorgetownX courses to the open source platform and broaden the course offerings available on edx.org.

    + +

    “We welcome Georgetown University to edX,” said Anant Agarwal, President of edX. “Georgetown has a long history of research and educational excellence, with a demonstrated commitment to the arts and sciences, foreign service, law, medicine, public policy, business, and nursing and health studies. Georgetown, with its distinguished presence around the world including a School of Foreign Service campus in Qatar, shares with edX a global perspective and a mission to expand educational opportunities.”

    + +

    Through edX, the “X Universities” will provide interactive education wherever there is access to the Internet. They will enhance teaching and learning through research about how students learn, and how technologies and game-like experiences can facilitate effective teaching both on-campus and online. The University of California, Berkeley joined edX in July, the University of Texas System joined in October, and Wellesley College joined earlier in December.

    + +

    “Georgetown University is an excellent addition to edX,” said MIT President L. Rafael Reif. “It brings important strength in many areas of scholarship and has long had an especially powerful voice in public life and discourse. The edX community stands to benefit greatly from what Georgetown will offer.”

    + +

    “EdX is an innovation that will expand access to high-quality educational content for millions around the world while helping us better understand how technology can improve the academic experience for students in classrooms across our campuses,” said Harvard President Drew Faust. “Georgetown’s commitment to technology enhanced learning, its excellence in education, and its long history as an institution dedicated to public service make it a welcome addition to edX.”

    + +

    GeorgetownX will offer courses on edX beginning in the fall of 2013. All of the courses will be hosted from edX’s innovative platform at www.edx.org.

    + +

    About edX

    + +

    edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT’s Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning-both on-campus and worldwide. EdX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.

    + +

    About Georgetown University

    + +

    Georgetown University is the oldest Catholic and Jesuit university in America, founded in 1789 by Archbishop John Carroll. Georgetown today is a major student-centered, international, research university offering respected undergraduate, graduate and professional programs from its home in Washington, D.C. For more information about Georgetown University, visit www.georgetown.edu.

    + +
    +

    Contact: Brad Baker

    +

    BBaker@webershandwick.com

    +

    617-520-7043

    +
    +
    + + +
    +
    +
    diff --git a/lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html b/lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html new file mode 100644 index 0000000000..77e7beb5f7 --- /dev/null +++ b/lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html @@ -0,0 +1,75 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">EdX expands platform, announces first wave of courses for spring 2013 +
    + + +
    +
    +

    EdX expands platform, announces first wave of courses for spring 2013

    +
    + +
    +

    Leading minds from top universities to offer world-wide MOOC courses on statistics, history, justice, and poverty

    + +

    CAMBRIDGE, MA – December 19, 2012 —EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today its initial spring 2013 schedule including its first set of courses in the humanities and social sciences – introductory courses with wide, global appeal. In its second semester, edX expands its online courses to a variety of subjects ranging from the ancient Greek hero to the riddle of world poverty, all taught by experts at some of the world’s leading universities. EdX is also bringing back several courses from its popular offerings in the fall semester.

    + +

    “EdX is both revolutionizing and democratizing education,” said Anant Agarwal, President of edX. “In just eight months we’ve attracted more than half a million unique users from around the world to our learning portal. Now, with these spring courses we are entering a new era – and are poised to touch millions of lives with the best courses from the best faculty at the best institutions in the world.”

    + +

    Building on the success of its initial offerings, edX is broadening the courses on its innovative educational platform. In its second semester – now open for registration – edX continues with courses from some of the world’s most esteemed faculty from UC Berkeley, Harvard and MIT. Spring 2013 courses include:

    + + + +

    “I'm delighted to have my Justice course on edX,” said Michael Sandel, Ann T. and Robert M. Bass Professor of Government at Harvard University, “where students everywhere will be able to engage in a global dialogue about the big moral and civic questions of our time.”

    + +

    In addition to these new courses, edX is bringing back several courses from the popular fall 2012 semester: Introduction to Computer Science and Programming; Introduction to Solid State Chemistry; Introduction to Artificial Intelligence; Software as a Service I; Software as a Service II; Foundations of Computer Graphics.

    + +

    This spring also features Harvard's Copyright, taught by Harvard Law School professor William Fisher III, former law clerk to Justice Thurgood Marshall and expert on the hotly debated U.S. copyright system, which will explore the current law of copyright and the ongoing debates concerning how that law should be reformed. Copyright will be offered as an experimental course, taking advantage of different combinations and uses of teaching materials, educational technologies, and the edX platform. 500 learners will be selected through an open application process that will run through January 3rd 2013.

    + +

    These new courses would not be possible without the contributions of key edX institutions, including UC Berkeley, which is the inaugural chair of the “X University” consortium and major contributor to the platform. All of the courses will be hosted on edX’s innovative platform at www.edx.org and are open for registration as of today. EdX expects to announce a second set of spring 2013 courses in the future.

    + +

    About edX

    + +

    EdX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.

    + +
    +

    Contact: Brad Baker

    +

    BBaker@webershandwick.com

    +

    617-520-7260

    +
    + + +
    +
    +
    diff --git a/lms/templates/university_profile/georgetownx.html b/lms/templates/university_profile/georgetownx.html new file mode 100644 index 0000000000..a519746c4c --- /dev/null +++ b/lms/templates/university_profile/georgetownx.html @@ -0,0 +1,24 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">GeorgetownX + +<%block name="university_header"> + + + + +<%block name="university_description"> +

    Georgetown University, the nation’s oldest Catholic and Jesuit university, is one of the world’s leading academic and research institutions, offering a unique educational experience that prepares the next generation of global citizens to lead and make a difference in the world.  Students receive a world-class learning experience focused on educating the whole person through exposure to different faiths, cultures and beliefs.

    + + +${parent.body()} diff --git a/lms/templates/video.html b/lms/templates/video.html index 18c1bcbced..38bc8cfcce 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -2,7 +2,9 @@

    ${display_name}

    % endif -
    +
    diff --git a/lms/urls.py b/lms/urls.py index bd414be789..14584baa52 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -37,6 +37,8 @@ urlpatterns = ('', url(r'^event$', 'track.views.user_track'), url(r'^t/(?P