From bd199ef5c3e8fada890aad2bde60f40187fc4d03 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Thu, 6 Sep 2012 14:19:18 -0400 Subject: [PATCH 01/66] Added opencourseware box on course about page --- lms/static/images/link-icon.png | Bin 0 -> 326 bytes lms/static/images/opencourseware.png | Bin 0 -> 11591 bytes .../sass/multicourse/_course_about.scss | 45 ++++++++++++++++-- lms/templates/portal/course_about.html | 15 ++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 lms/static/images/link-icon.png create mode 100644 lms/static/images/opencourseware.png diff --git a/lms/static/images/link-icon.png b/lms/static/images/link-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..976197f2c8b556035bd8edce82962cf3292f25dc GIT binary patch literal 326 zcmV-M0lEH(P)1 zy)Fb{6vpwRqY|1$NHk=ZLJ-*mp;(Q=RY=BFh-ih-n1VuK;;W%hxgOtkM`X~x{>3xN z$s3t1ewoQRdFCYNj3r6Z%@i^`;|b?zz`6xj(MG@ze)ax}MpbYIj|e!$Fh-E6c2gDZ z!x)yLdOtq#@n2{Z+qgxF6~rHHBaeg2T1tqd=z-xtwR<0IB3C2BBDPTA8ipP9zcKG8 zmRmhiZCuYuP2E+$z&gxyvyK{v3Y_2zo0vuF&b0dm@Bjb+ literal 0 HcmV?d00001 diff --git a/lms/static/images/opencourseware.png b/lms/static/images/opencourseware.png new file mode 100644 index 0000000000000000000000000000000000000000..6c03669e4da815d4a33ed436712b0ea66869995a GIT binary patch literal 11591 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 section { + @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15)); + border: 1px solid rgb(200,200,200); + + &.course-summary { + padding: 16px 20px 30px; + margin-bottom: 40px; + border-top: none; + } + + &.additional-resources { + padding: 30px; + + .opencourseware { + text-indent: -9999px; + background: url('../images/opencourseware.png') 0 0 no-repeat; + width: 266px; + height: 31px; + margin-bottom: 20px; + } + + ul { + padding-left: 0; + margin-bottom: 0; + } + + li { + list-style: none; + padding-left: 29px; + background: url('../images/link-icon.png') left center no-repeat; + } + } + } + header { margin-bottom: 30px; padding-bottom: 16px; @@ -441,6 +471,13 @@ } } } + + h1 { + font: 1em $serif; + letter-spacing: 0; + color: #999; + margin-bottom: 0; + } } .important-dates { diff --git a/lms/templates/portal/course_about.html b/lms/templates/portal/course_about.html index ac7b9090d0..bc15e16f5e 100644 --- a/lms/templates/portal/course_about.html +++ b/lms/templates/portal/course_about.html @@ -151,6 +151,21 @@ % endif + +
+
+

Additional Resources

+
+ +
+

MITOpenCourseware

+ +
+
From 81ad9530d6c5dd0b78685ced720903dbd2619426 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 7 Sep 2012 09:49:35 -0400 Subject: [PATCH 02/66] Some edits for the resources box --- lms/static/images/opencourseware.png | Bin 11591 -> 13203 bytes .../sass/multicourse/_course_about.scss | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/images/opencourseware.png b/lms/static/images/opencourseware.png index 6c03669e4da815d4a33ed436712b0ea66869995a..b209bee85771212c854c5b48c1e6221d8133bcc0 100644 GIT binary patch delta 12618 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 G2K^7D@ac2_ delta 10993 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

aM>`II zSi?hWJz>TcS|$26RZy<;m*0IsI8M_IGC|w|XJ4m^78+BE0L5A$knI_Ce1Zr%I+7G; zrsJ9@asXHcpjGhl$<*ZL(a>Is4dqg{kcEtf{W8eR$@?Tzr6W2!Yba;>A#a9q#il2h z&*uxq;2U3~sJE|_7Hv}AQY)1@Io}{?D~wZOcU~_q36vg#@S2DRuifyL`a zj{f=Aw5K;;Jh|qn2(j^jc@NR*9L#9zU0q_9%RTa!K9DAin1c|vqQ`%j<;o&8Y@He9 zs8zK31o8iAu=lRf7hPoeG*v%SzRCNhDnr^x$m(r{u^;6+vPGKcKT{4a7wDGu1hGeB znO0!Ot(QjVV*bVGk{5oDC-vAxc;`HaL{6})Kcm#(g7cL~1%1Vbh9T1}Yp3D117gKz zG9?X7wm`n<+W_$kR^(4Hi5K0DmwSmRpj#;uAyZmj>{r|>QPtkqZ5Z|qPGVb+BCvfU zjUU=rH%pL}cfPb>P!)MjE;{jFq?lFbRUY3K9x}w;?e+J3$l2sKir)e}pcHmb(dB+7 zrZ^bnEEAS9qZO*nq-}dAZ6Y5L?BXpr-%s=}|1+Pk^v=$a#)v1v`Uv-!zzdz$pV+?3 zt$@Gdd77V5kC89g@s81Z-a-&P`^5j*9;>pwcK^08MmO51xjrQ#yZ_PzQQ<6#*!u(n zaa~6E+c0Qj`_|saDMz*!xPx{6m|6|L(6MVZ3?4JDywdECuw!1eJO1`LhHQ76_DhDI z6ObxK7TsiWlp1HM>k^qNscq);iW(e-zvYF|-(_>Bso~g0l=Fk(R>9uVO>kLZ&&JYJ zr2YVdq`2+%H_;V#QA?{2BQY2wKhFyP6@9M zKq)Jww<0?1T_2J{=&#&83Vr-&rPVK|*8!ccw}0O4rn{lNo1$fmQX0iN^$x!jkWjjf zPoF8p;*_YA$8kc3%66v*{O9@V;p$L4u=s|A95z*9Rovc&mn`TGcyGdWUXDli0#?&K1|Jbv~%e?PK7(03GXlfo_HMdp3&H*T( z*ilffQLg76uX$o|&Ucgwda`hJm#~@kUn6iko@CJsy+^+4AT15{_f58N>MhdgXzh2` z9lQm^>&J0ZL(AYHrJsWgLR65Mb`%J;_%6NyY+V=^RL}L zoM6jy&#!516-)UHIQt0Yeco0Ov^$|i%e!%rhs!0<%*oQ*e-{@Y0WkZE3u@N9VvKvG ziCUgs>?7%@?dXsbck?-xK~YohJkeyB#U7i)))vKYte<~EvoKQ2x%ARq+6K+ev1Qur z?}UH^j71SA0r`2fY)R`SWwiyhi2e zm`^@LGyetX+Q-+)HZO}47BAB=+DnZKU8#}*FU9CPU%$5)<&xWQ7hD%-riuyz4g60W@6gTIX7fO?{W|fn=g+sh zzx{XWUH@94mYd}9EQFIqYO8mw8WwCE{2<^W@zWp&knXlg$+T{Ey1)amo!C@=p(FDP z_-=E@jRmvj(;}RmYJerOUXe*Ai=pEA1o#Pf2eo}sorQwaAXA3o;*3^9p{ANV9NI_O z!QcL!3*qp6&#%$7FR5~ILDY>&nBk4yT|#e+zb)QLJb>80&Tboif0Ev3L)HF7$J{_j zF$yySS9evOHy=|qlEB%nB_!N!5nsNi#>dH+q;!6L)r@0eDgcUMwKprxc7A+s{+I6f z01O?&wiCsH{44oNB$yuZoHe*?f%?{nNo(c}u zu@EOC%D>h}i;3m>SC$r4CfHGW{0fy)$G!&vOo_9GM!cxbRckXQ*Dm!F(vVaI7KRs| zGOXw-K^Sd2?4ua@q!Aus>18Ev6&{(lDH6fG*y@qYA90m#sYK$>iSs21^m?$X#x8hd zR~w>X$o6aq{TdI_Q^Ec(X^YRoi1jK`OI{d_gL;w5Q*Yhe!D{uOAzB8uBa#>RUn)8v zdq7Tv7+`DNx^5KY@gAI)Z39vhx^baNkgH@edZfo>_e!?rKmL)aEUyM!Wx3p13-IR7 z^xM(%o@93AN-eXqY|URzZ~r&Za%5cFh`u%@X!P>BCAYq~AiT5Pw}#W%tUFLzrQ5yE@lj;o#@Fl~Qu{f1Qhkd`u3l7g1P ztF+}_TTmj_{87maNEXjeTUhRUt`{Q58~KSCJLc!_=raf@;)39W;(l^qpceQevhvO- zh2(I)R{0MBo&YGnlZMO=r%G0q_BA2%=YVjY9yXvU16k|GpApE$B#mqf6}u*(VmSYo zs7FRqx)X+8#65M+)4RuY=v1DewQ`C^h6fmA448|)>yKd@A1d($0{W1IQcl;%<6ipgtH%i{}rJ`UK@)pvHOE-Y*vF=ghh zj-&a7S1ct$8I43}R{NXp1DPy5318bXj6HE^C^!lepZaTMRU?Ic+4#`JOHCb}=(MD0 z%7i{minz})Q<)F+d=hF3t6GM^?98$lPvkgvWHhsA?mY1ts{m2+o;sOEp&Ftt9s?4+ zi38=ot{PdF8@DZtJBYe+tyW{P2a^PWKX@YY`2ry@YokB^{m0ZNp~v1!q48@JJ4tX`3yGbsF68Nl2&7N*=z0%o#KcWT0i@nT7{B~RYfZYW9mC+E#-)=)O%X7Y#Dv{6_lMjS2b$#e92+t&PGAkA<5qDZ zQv3~NTqP<&R9rVe>U?|`V|y0@YgP5+-Xmy&W}h4m-37`v+59M_jq?8K$PPfkrjF52 zwTtcJkz42$pRw^qoOw9()FYan=kkmw;>iwrk7)zi#$NnCDEYxE{_Y`sWK)@*FC+_Q zLbua`%!>g5^Unyw^}kWUQQF7P~jTkLP;odpJ$Tz8oOPC+VuP+1VKK7?Yyw=5HMm*H5_xyPF?B2 z0>J;(A12{T!c2V@VGiRk0)MAuVl45bR3z3+N>x)O>EpJ<*LMpUvN$vem@eKKm&tis7u`CS%jC{+Z%u zM|FWFvO(te=C~%EsRT?BHE9zt-L3+oO&~uZ5hw0<`xfmAtnJZg4JDdms%M1CNQcvMD<(HcSXgC1jw?>jd+&Y}~RHJN4=lNmEj zKM9(%%i}EfGw+eMhx0O<=ow%8CN|SQCS#ECpC^)Zm{4C4$}}?OSn%?R3I#A20E$p4 zbT)phbq2+ozw*(zN~=X$D;cd+o}?*R728w&BMd4VLas(;#^33h4o$eekTZ)WDEX6A z{45kU8ZmE*imh$V^!4^`(#5HJGxru!AfNDATbP}&Yb8jmkOavx0)p?GH7XNBqOUrU zW`{B1M;GOl3t`^sg@oZH3rS42K#-H(%QQKzI{t^w5|R#$4kANdmcfCRPt{vsO*%bZ zO$5j~C-(YWTAw^Ol2@zO4#%PFqPoFuv1pttY~6#*@c1&6u}C4KBfX6zjhHpvd8GjZ z!Xyg^jdJQ0(SHB=XK}F^D)arICQYf(?%AmU8eG{81qESId7`2Vbb`46|ES{YxBcC< zAgb0+8=2(H+%jS=Co z{lHBvQu$w3yyXc;+|2?&XOh{1v7SS(u!Xs? z3o{B)lJThYtchD1db9biT>#6F4KiHq`Rz}{SZqMt2Q3XTdF)tyyJZpR9zF7_mbU4A zAbh-lOFT@i5NHHn5na=LNrGVGX3V;8frc;XKMoU|(*OD$Zka8DQA>aqA}km71P z_N@PV;`v4PXnU6_+J_EmNr~aa!o}0vJSagR$?(VL-STY-fj>>!m%9#gkr(MaDs<-%)SO`nar#(gZjdx;jfgi zAB5|kn}Z0J^K%k-TKZpP_sFW1zOFlxpa(R=Qar4<#DQ-)ZfDu&kj8D{bB9y>b^prM z1qH|a+Jb}o!xc(bBG&asUfxS?)tDUV;k>y!xdq&|l&$Mla|VNV?&;Gz6O};R|K1Wx z{qK42!uZDG%Q5H=88suNUl?!?pE0w23p0;X>z>Z|%v>yu6A>%8%*$4oSY&!#J>x~* zUG05Q?xX+e*D(32m1_5pZ`8OIfeb;?bFEYD*I2MP&W=~n1c}lDuAz_WV9q#Br21HzXSdqcW=>U#35k$=pl2|ASfb3+!gOrYK1{ z>WD!u>`xHDC%m$}l9V?7Zr~j~=ncvGRc{o)INe1EU`GcKnzxBWkJW8nz+g)Q9D5+m zDFGAa{8oxWeVmk{=Nkg!rdgGaI?)8<{mgY{f{zujHOn+2X|6`(lrwP^EjzdV_Fs;k zvAEua!t#ngxDHT)6W<1Ra)b>hLJ=D^35MnyfcjEFc#BJ7%YQ~@O><}()uL|qm;gKU z!9F5p={wHP~GOM>@+!;&?s82w-5fk?23=76SF;qhs#iUc=cqSoCN z`@bMJeFiZF^UpokZ--K0Z%~B!`9vCxOkijR8r17|>QnTDir!b6yGg96f1`P|`Hy%g z!H*b{>Wracf_F2xc>6;LYT7Aw~@hN*zp&1)-0fM8+UI_-!i73bI}dt7N0s;2Ar?V+RfO4}|bhDBzS;kQE%V+W(Kf33?wyEFzn_>lNsAwx! IE4~i>f80}YrT_o{ diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss index b08512c58d..d23917fe27 100644 --- a/lms/static/sass/multicourse/_course_about.scss +++ b/lms/static/sass/multicourse/_course_about.scss @@ -344,7 +344,7 @@ &.course-summary { padding: 16px 20px 30px; - margin-bottom: 40px; + margin-bottom: 220px; border-top: none; } From ad5b3a334203394792c90b0d1bfe2dda8efe13b3 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 10:30:18 -0400 Subject: [PATCH 03/66] add admin interface for tracking logs --- common/djangoapps/track/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 common/djangoapps/track/admin.py diff --git a/common/djangoapps/track/admin.py b/common/djangoapps/track/admin.py new file mode 100644 index 0000000000..1f19c59a93 --- /dev/null +++ b/common/djangoapps/track/admin.py @@ -0,0 +1,8 @@ +''' +django admin pages for courseware model +''' + +from track.models import * +from django.contrib import admin + +admin.site.register(TrackingLog) From ab0a58fb7a01d7167906d8fe911128e743cf8977 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 22:31:45 -0400 Subject: [PATCH 04/66] add psychometrics - grade histograms, check time diffs, and IRT plots --- common/lib/xmodule/xmodule/capa_module.py | 3 + lms/djangoapps/courseware/module_render.py | 4 + lms/djangoapps/instructor/views.py | 34 +++++- lms/envs/common.py | 3 + lms/envs/dev.py | 2 + .../courseware/instructor_dashboard.html | 105 ++++++++++++++++-- 6 files changed, 142 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d186bcc39c..8e2d12d5e9 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -509,6 +509,9 @@ class CapaModule(XModule): event_info['success'] = success self.system.track_function('save_problem_check', event_info) + if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback + self.system.psychometrics_handler(self.get_instance_state()) + # render problem into HTML html = self.get_problem_html(encapsulate=False) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 88368c4a80..b033660c17 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -16,6 +16,7 @@ from capa.xqueue_interface import XQueueInterface from courseware.access import has_access from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache +from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from static_replace import replace_urls from xmodule.errortracker import exc_info_to_str from xmodule.exceptions import NotFoundError @@ -230,6 +231,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) + if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS') and instance_module is not None: + system.set('psychometrics_handler', # set callback for updating PsychometricsData + make_psychometrics_data_update_handler(instance_module)) try: module = descriptor.xmodule_constructor(system)(instance_state, shared_state) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f4e9c27991..74f186b689 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -27,6 +27,7 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access, get_access_group_name from courseware.courses import (get_course_with_access, get_courses_by_university) +from psychometrics import psychoanalyze from student.models import UserProfile from student.models import UserTestGroup, CourseEnrollment @@ -51,7 +52,18 @@ def instructor_dashboard(request, course_id): instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists msg = '' - # msg += ('POST=%s' % dict(request.POST)).replace('<','<') + #msg += ('POST=%s' % dict(request.POST)).replace('<','<') + + problems = [] + plots = [] + + # the instructor dashboard page is modal: grades, psychometrics, admin + # keep that state in request.session (defaults to grades mode) + idash_mode = request.POST.get('idash_mode','') + if idash_mode: + request.session['idash_mode'] = idash_mode + else: + idash_mode = request.session.get('idash_mode','Grades') def escape(s): """escape HTML special characters in string""" @@ -149,6 +161,9 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') return return_csv('answer_dist_%s.csv' % course_id, get_answers_distribution(request, course_id)) + #---------------------------------------- + # Admin + elif 'List course staff' in action: group = get_staff_group(course) msg += 'Staff group = %s' % group.name @@ -187,13 +202,28 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff %s' % user, {}, page='idashboard') - # For now, mostly a static page + #---------------------------------------- + # psychometrics + + elif action == 'Generate Histogram and IRT Plot': + problem = request.POST['Problem'] + nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) + msg += nmsg + + if idash_mode=='Psychometrics': + problems = psychoanalyze.problems_with_psychometric_data(course_id) + + #---------------------------------------- + # context for rendering context = {'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'datatable': datatable, 'msg': msg, + 'modeflag': {idash_mode: 'selectedmode'}, + 'problems': problems, # psychometrics + 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), } diff --git a/lms/envs/common.py b/lms/envs/common.py index 63301d420b..3cfaae940d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -71,6 +71,8 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION' : False, 'ENABLE_DISCUSSION_SERVICE': True, + 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) + 'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_LMS_MIGRATION': False, 'ENABLE_MANUAL_GIT_RELOAD': False, @@ -619,6 +621,7 @@ INSTALLED_APPS = ( 'util', 'certificates', 'instructor', + 'psychometrics', #For the wiki 'wiki', # The new django-wiki from benjaoming diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 50befeb875..5a7e019e55 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -20,6 +20,8 @@ MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains- MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True +MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = True # real-time psychometrics (eg item response theory analysis in instructor dashboard) + WIKI_ENABLED = True diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 8568490e5e..d73eda1ed7 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -4,6 +4,8 @@ <%block name="headextra"> <%static:css group='course'/> + + <%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> @@ -31,37 +33,89 @@ table.stat_table td { border-color: #666666; background-color: #ffffff; } + +a.selectedmode { background-color: yellow; } + + +

+

Instructor Dashboard

-
- +

[ Grades | + %if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): + Psychometrics | + %endif + Admin ] +

+ + + + +##----------------------------------------------------------------------------- +%if modeflag.get('Grades'):

Gradebook +

Grade summary +

+

+

+

+

+ %endif -%if instructor_access: +##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics'): + +

Select a problem and an action: +

+ +

+ +

+

+ +

+ +

+ +%endif + +##----------------------------------------------------------------------------- +%if modeflag.get('Admin'): + %if instructor_access:

@@ -69,16 +123,20 @@ table.stat_table td {


- %endif + %endif -%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access: + %if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:

+ %endif %endif

+##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics') is None: +

@@ -99,14 +157,45 @@ table.stat_table td { %endfor

+%endif + +##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics'): + + %for plot in plots: +
+

${plot['title']}

+
+

${plot['info']}

+
+
+ +
+
+ %endfor + +%endif + +##----------------------------------------------------------------------------- +## always show msg %if msg:

${msg}

%endif -% if course_errors is not UNDEFINED: +##----------------------------------------------------------------------------- +%if modeflag.get('Admin'): + % if course_errors is not UNDEFINED:

Course errors

+ %if not course_errors: + None + %else:
    % for (summary, err) in course_errors:
  • ${summary | h} @@ -118,8 +207,10 @@ table.stat_table td {
  • % endfor
+ %endif
-% endif + % endif +%endif
From 0bf85992da090b624fb915cb87a1323736b6a67c Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 22:32:28 -0400 Subject: [PATCH 05/66] psychometrics djangoapp --- lms/djangoapps/psychometrics/__init__.py | 0 lms/djangoapps/psychometrics/admin.py | 8 + .../psychometrics/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/init_psychometrics.py | 66 ++++ lms/djangoapps/psychometrics/models.py | 45 +++ lms/djangoapps/psychometrics/psychoanalyze.py | 312 ++++++++++++++++++ 7 files changed, 431 insertions(+) create mode 100644 lms/djangoapps/psychometrics/__init__.py create mode 100644 lms/djangoapps/psychometrics/admin.py create mode 100644 lms/djangoapps/psychometrics/management/__init__.py create mode 100644 lms/djangoapps/psychometrics/management/commands/__init__.py create mode 100644 lms/djangoapps/psychometrics/management/commands/init_psychometrics.py create mode 100644 lms/djangoapps/psychometrics/models.py create mode 100644 lms/djangoapps/psychometrics/psychoanalyze.py diff --git a/lms/djangoapps/psychometrics/__init__.py b/lms/djangoapps/psychometrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/admin.py b/lms/djangoapps/psychometrics/admin.py new file mode 100644 index 0000000000..ff1a14d722 --- /dev/null +++ b/lms/djangoapps/psychometrics/admin.py @@ -0,0 +1,8 @@ +''' +django admin pages for courseware model +''' + +from psychometrics.models import * +from django.contrib import admin + +admin.site.register(PsychometricData) diff --git a/lms/djangoapps/psychometrics/management/__init__.py b/lms/djangoapps/psychometrics/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/management/commands/__init__.py b/lms/djangoapps/psychometrics/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py new file mode 100644 index 0000000000..b7c9779d08 --- /dev/null +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# +# generate pyschometrics data from tracking logs and student module data + +import os, sys, string +import datetime +import json + +from courseware.models import * +from track.models import * +from psychometrics.models import * +from xmodule.modulestore import Location + +from django.core.management.base import BaseCommand + +#db = "ocwtutor" # for debugging +db = "default" + +class Command(BaseCommand): + help = "initialize PsychometricData tables from StudentModule instances (and tracking data, if in SQL)." + help += "Note this is done for all courses for which StudentModule instances exist." + + def handle(self, *args, **options): + + # delete all pmd + + #PsychometricData.objects.all().delete() + #PsychometricData.objects.using(db).all().delete() + + smset = StudentModule.objects.using(db).exclude(max_grade=None) + + for sm in smset: + url = sm.module_state_key + location = Location(url) + if not location.category=="problem": + continue + try: + state = json.loads(sm.state) + done = state['done'] + except: + print "Oops, failed to eval state for %s (state=%s)" % (sm,sm.state) + continue + + if done: # only keep if problem completed + try: + pmd = PsychometricData.objects.using(db).get(studentmodule=sm) + except PsychometricData.DoesNotExist: + pmd = PsychometricData(studentmodule=sm) + + pmd.done = done + pmd.attempts = state['attempts'] + + # get attempt times from tracking log + uname = sm.student.username + tset = TrackingLog.objects.using(db).filter(username=uname, event_type__contains='save_problem_check') + tset = tset.filter(event_source='server') + tset = tset.filter(event__contains="'%s'" % url) + checktimes = [x.dtcreated for x in tset] + pmd.checktimes = json.dumps(checktimes) + if not len(checktimes)==pmd.attempts: + print "Oops, mismatch in number of attempts and check times for %s" % pmd + + #print pmd + pmd.save(using=db) + + print "%d PMD entries" % PsychometricData.objects.using(db).all().count() diff --git a/lms/djangoapps/psychometrics/models.py b/lms/djangoapps/psychometrics/models.py new file mode 100644 index 0000000000..4ffdf59120 --- /dev/null +++ b/lms/djangoapps/psychometrics/models.py @@ -0,0 +1,45 @@ +# +# db model for psychometrics data +# +# this data is collected in real time +# + +from django.db import models +from courseware.models import StudentModule + +class PsychometricData(models.Model): + """ + This data is a table linking student, module, and module performance, + including number of attempts, grade, max grade, and time of checks. + + Links to instances of StudentModule, but only those for capa problems. + + Note that StudentModule.module_state_key is nominally a Location instance (url string). + That means it is of the form {tag}://{org}/{course}/{category}/{name}[@{revision}] + and for capa problems, category = "problem". + + checktimes is extracted from tracking logs, or added by capa module via psychometrics callback. + """ + + studentmodule = models.ForeignKey(StudentModule, db_index=True, unique=True) # contains student, module_state_key, course_id + + done = models.BooleanField(default=False) + attempts = models.IntegerField(default=0) # extracted from studentmodule.state + checktimes = models.TextField(null=True, blank=True) # internally stored as list of datetime objects + + # keep in mind + # grade = studentmodule.grade + # max_grade = studentmodule.max_grade + # student = studentmodule.student + # course_id = studentmodule.course_id + # location = studentmodule.module_state_key + + def __unicode__(self): + sm = self.studentmodule + return "[PsychometricData] %s url=%s, grade=%s, max=%s, attempts=%s, ct=%s" % (sm.student, + sm.module_state_key, + sm.grade, + sm.max_grade, + self.attempts, + self.checktimes) + diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py new file mode 100644 index 0000000000..e8dd7b4684 --- /dev/null +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -0,0 +1,312 @@ +# +# File: psychometrics/psychoanalyze.py +# +# generate pyschometrics plots from PsychometricData + +from __future__ import division + +import datetime +import logging +import json +import math +import numpy as np +from scipy.optimize import curve_fit + +from django.db.models import Sum, Max +from psychometrics.models import * +from xmodule.modulestore import Location + +log = logging.getLogger("mitx.psychometrics") + +#db = "ocwtutor" # for debugging +db = "default" + +#----------------------------------------------------------------------------- +# fit functions + +def func_2pl(x,a,b): + """ + 2-parameter logistic function + """ + D = 1.7 + edax = np.exp(D*a*(x-b)) + return edax / (1+edax) + +#----------------------------------------------------------------------------- +# statistics class + +class StatVar(object): + """ + Simple statistics on floating point numbers: avg, sdv, var, min, max + """ + def __init__(self,unit=1): + self.sum = 0 + self.sum2 = 0 + self.cnt = 0 + self.unit = unit + self.min = None + self.max = None + def add(self,x): + if x is None: + return + if self.min is None: + self.min = x + else: + if xself.max: + self.max = x + self.sum += x + self.sum2 += x**2 + self.cnt += 1 + def avg(self): + if self.cnt is None: + return 0 + return self.sum / 1.0 / self.cnt / self.unit + def var(self): + if self.cnt is None: + return 0 + return (self.sum2 / 1.0 / self.cnt / (self.unit**2)) - (self.avg()**2) + def sdv(self): + v = self.var() + if v>0: + return math.sqrt(v) + else: + return 0 + def __str__(self): + return 'cnt=%d, avg=%f, sdv=%f' % (self.cnt,self.avg(),self.sdv()) + def __add__(self,x): + self.add(x) + return self + +#----------------------------------------------------------------------------- +# histogram generator + +def make_histogram(ydata,bins=None): + ''' + Generate histogram of ydata using bins provided, or by default bins + from 0 to 100 by 10. bins should be ordered in increasing order. + + returns dict with keys being bins, and values being counts. + special: hist['bins'] = bins + ''' + if bins is None: + bins = range(0,100,10) + + nbins = len(bins) + hist = dict(zip(bins,[0] * nbins)) + for y in ydata: + for b in bins[::-1]: # in reverse order + if y>b: + hist[b] += 1 + break + # hist['bins'] = bins + return hist + +#----------------------------------------------------------------------------- + +def problems_with_psychometric_data(course_id): + ''' + Return dict of {problems (location urls): count} for which psychometric data is available. + Does this for a given course_id. + ''' + pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id) + plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()] + problems = dict( (p,pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist ) + + return problems + +#----------------------------------------------------------------------------- + +def generate_plots_for_problem(problem): + + pmdset = PsychometricData.objects.using(db).filter(studentmodule__module_state_key=problem) + nstudents = pmdset.count() + msg = "" + plots = [] + + if nstudents < 2: + msg += "%s nstudents=%d --> skipping, too few" % (problem,nstudents) + return msg, plots + + max_grade = pmdset[0].studentmodule.max_grade + + agdat = pmdset.aggregate(Sum('attempts'), Max('attempts')) + max_attempts = agdat['attempts__max'] + total_attempts = agdat['attempts__sum'] # not used yet + + msg += "max attempts = %d" % max_attempts + + xdat = range(1,max_attempts+1) + dataset = {'xdat': xdat} + + # generate grade histogram + ghist = [] + + axisopts = """{ + xaxes: [{ + axisLabel: 'Grade' + }], + yaxes: [{ + position: 'left', + axisLabel: 'Count' + }] + }""" + + if max_grade > 1: + ghist = make_histogram([pmd.studentmodule.grade for pmd in pmdset],np.linspace(0,max_grade,max_grade+1)) + ghist_json = json.dumps(ghist.items()) + + plot = {'title': "Grade histogram for %s" % problem, + 'id': 'histogram', + 'info': '', + 'data': "var dhist = %s;\n" % ghist_json, + 'cmd': "[ {data: dhist, bars: { show: true }} ], %s" % axisopts, + } + plots.append(plot) + else: + msg += "
Not generating histogram: max_grade=%s" % max_grade + + # histogram of time differences between checks + # Warning: this is inefficient - doesn't scale to large numbers of students + dtset = [] # time differences in minutes + dtsv = StatVar() + for pmd in pmdset: + try: + checktimes = eval(pmd.checktimes) # update log of attempt timestamps + except: + continue + if len(checktimes)<2: + continue + ct0 = checktimes[0] + for ct in checktimes[1:]: + dt = (ct-ct0).total_seconds()/60.0 + if dt<20: # ignore if dt too long + dtset.append(dt) + dtsv += dt + ct0 = ct + if dtsv.cnt > 2: + msg += "
time differences between checks: %s" % dtsv + bins = np.linspace(0,1.5*dtsv.sdv(),30) + dbar = bins[1]-bins[0] + thist = make_histogram(dtset,bins) + thist_json = json.dumps(sorted(thist.items(), key=lambda(x): x[0])) + + axisopts = """{ xaxes: [{ axisLabel: 'Time (min)'}], yaxes: [{position: 'left',axisLabel: 'Count'}]}""" + + plot = {'title': "Histogram of time differences between checks", + 'id': 'thistogram', + 'info': '', + 'data': "var thist = %s;\n" % thist_json, + 'cmd': "[ {data: thist, bars: { show: true, barWidth:%f }} ], %s" % (dbar, axisopts), + } + plots.append(plot) + + # one IRT plot curve for each grade received (TODO: this assumes integer grades) + for grade in range(1,int(max_grade)+1): + yset = {} + gset = pmdset.filter(studentmodule__grade=grade) + ngset = gset.count() + if ngset==0: + continue + ydat = [] + ylast = 0 + for x in xdat: + y = gset.filter(attempts=x).count()/ngset + ydat.append( y + ylast ) + ylast = y + ylast + yset['ydat'] = ydat + + if len(ydat)>5: # try to fit to logistic function if enough data points + cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts/2.0]) + yset['fitparam'] = cfp + yset['fitpts'] = func_2pl(np.array(xdat),*cfp[0]) + yset['fiterr'] = [yd-yf for (yd,yf) in zip(ydat,yset['fitpts'])] + fitx = np.linspace(xdat[0],xdat[-1],100) + yset['fitx'] = fitx + yset['fity'] = func_2pl(np.array(fitx),*cfp[0]) + + dataset['grade_%d' % grade] = yset + + axisopts = """{ + xaxes: [{ + axisLabel: 'Number of Attempts' + }], + yaxes: [{ + max:1.0, + position: 'left', + axisLabel: 'Probability of correctness' + }] + }""" + + # generate points for flot plot + for grade in range(1,int(max_grade)+1): + jsdata = "" + jsplots = [] + gkey = 'grade_%d' % grade + if gkey in dataset: + yset = dataset[gkey] + jsdata += "var d%d = %s;\n" % (grade,json.dumps(zip(xdat,yset['ydat']))) + jsplots.append('{ data: d%d, lines: { show: false }, points: { show: true}, color: "red" }' % grade) + if 'fitpts' in yset: + jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'],yset['fity']))) + jsplots.append('{ data: fit, lines: { show: true }, color: "blue" }') + (a,b) = yset['fitparam'][0] + irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a,b) + else: + irtinfo = "" + + plots.append({'title': 'IRT Plot for grade=%s %s' % (grade,irtinfo), + 'id': "irt%s" % grade, + 'info': '', + 'data': jsdata, + 'cmd' : '[%s], %s' % (','.join(jsplots), axisopts), + }) + + #log.debug('plots = %s' % plots) + return msg, plots + +#----------------------------------------------------------------------------- + +def make_psychometrics_data_update_handler(studentmodule): + """ + Construct and return a procedure which may be called to update + the PsychometricsData instance for the given StudentModule instance. + """ + sm = studentmodule + try: + pmd = PsychometricData.objects.using(db).get(studentmodule=sm) + except PsychometricData.DoesNotExist: + pmd = PsychometricData(studentmodule=sm) + + def psychometrics_data_update_handler(state): + """ + This function may be called each time a problem is successfully checked + (eg on save_problem_check events in capa_module). + + state = instance state (a nice, uniform way to interface - for more future psychometric feature extraction) + """ + try: + state = json.loads(sm.state) + done = state['done'] + except: + log.exception("Oops, failed to eval state for %s (state=%s)" % (sm,sm.state)) + return + + pmd.done = done + pmd.attempts = state['attempts'] + try: + checktimes = eval(pmd.checktimes) # update log of attempt timestamps + except: + checktimes = [] + checktimes.append(datetime.datetime.now()) + pmd.checktimes = checktimes + try: + pmd.save() + except: + log.exception("Error in updating psychometrics data for %s" % sm) + + return psychometrics_data_update_handler From b4a3591310203732ef510488ba7b0da1158b8e50 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 23:21:13 -0400 Subject: [PATCH 06/66] add jquery flot axislabels plugin --- .../js/vendor/flot/jquery.flot.axislabels.js | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 common/static/js/vendor/flot/jquery.flot.axislabels.js diff --git a/common/static/js/vendor/flot/jquery.flot.axislabels.js b/common/static/js/vendor/flot/jquery.flot.axislabels.js new file mode 100644 index 0000000000..797f82ec9f --- /dev/null +++ b/common/static/js/vendor/flot/jquery.flot.axislabels.js @@ -0,0 +1,412 @@ +/* +Axis Labels Plugin for flot. +http://github.com/markrcote/flot-axislabels + +Original code is Copyright (c) 2010 Xuan Luo. +Original code was released under the GPLv3 license by Xuan Luo, September 2010. +Original code was rereleased under the MIT license by Xuan Luo, April 2012. + +Improvements by Mark Cote. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ +(function ($) { + var options = { }; + + function canvasSupported() { + return !!document.createElement('canvas').getContext; + } + + function canvasTextSupported() { + if (!canvasSupported()) { + return false; + } + var dummy_canvas = document.createElement('canvas'); + var context = dummy_canvas.getContext('2d'); + return typeof context.fillText == 'function'; + } + + function css3TransitionSupported() { + var div = document.createElement('div'); + return typeof div.style.MozTransition != 'undefined' // Gecko + || typeof div.style.OTransition != 'undefined' // Opera + || typeof div.style.webkitTransition != 'undefined' // WebKit + || typeof div.style.transition != 'undefined'; + } + + + function AxisLabel(axisName, position, padding, plot, opts) { + this.axisName = axisName; + this.position = position; + this.padding = padding; + this.plot = plot; + this.opts = opts; + this.width = 0; + this.height = 0; + } + + + CanvasAxisLabel.prototype = new AxisLabel(); + CanvasAxisLabel.prototype.constructor = CanvasAxisLabel; + function CanvasAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, padding, + plot, opts); + } + + CanvasAxisLabel.prototype.calculateSize = function() { + if (!this.opts.axisLabelFontSizePixels) + this.opts.axisLabelFontSizePixels = 14; + if (!this.opts.axisLabelFontFamily) + this.opts.axisLabelFontFamily = 'sans-serif'; + + var textWidth = this.opts.axisLabelFontSizePixels + this.padding; + var textHeight = this.opts.axisLabelFontSizePixels + this.padding; + if (this.position == 'left' || this.position == 'right') { + this.width = this.opts.axisLabelFontSizePixels + this.padding; + this.height = 0; + } else { + this.width = 0; + this.height = this.opts.axisLabelFontSizePixels + this.padding; + } + }; + + CanvasAxisLabel.prototype.draw = function(box) { + var ctx = this.plot.getCanvas().getContext('2d'); + ctx.save(); + ctx.font = this.opts.axisLabelFontSizePixels + 'px ' + + this.opts.axisLabelFontFamily; + var width = ctx.measureText(this.opts.axisLabel).width; + var height = this.opts.axisLabelFontSizePixels; + var x, y, angle = 0; + if (this.position == 'top') { + x = box.left + box.width/2 - width/2; + y = box.top + height*0.72; + } else if (this.position == 'bottom') { + x = box.left + box.width/2 - width/2; + y = box.top + box.height - height*0.72; + } else if (this.position == 'left') { + x = box.left + height*0.72; + y = box.height/2 + box.top + width/2; + angle = -Math.PI/2; + } else if (this.position == 'right') { + x = box.left + box.width - height*0.72; + y = box.height/2 + box.top - width/2; + angle = Math.PI/2; + } + ctx.translate(x, y); + ctx.rotate(angle); + ctx.fillText(this.opts.axisLabel, 0, 0); + ctx.restore(); + }; + + + HtmlAxisLabel.prototype = new AxisLabel(); + HtmlAxisLabel.prototype.constructor = HtmlAxisLabel; + function HtmlAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + HtmlAxisLabel.prototype.calculateSize = function() { + var elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + // store height and width of label itself, for use in draw() + this.labelWidth = elem.outerWidth(true); + this.labelHeight = elem.outerHeight(true); + elem.remove(); + + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelWidth + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + HtmlAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); + var elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + if (this.position == 'top') { + elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px'); + elem.css('top', box.top + 'px'); + } else if (this.position == 'bottom') { + elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px'); + elem.css('top', box.top + box.height - this.labelHeight + 'px'); + } else if (this.position == 'left') { + elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px'); + elem.css('left', box.left + 'px'); + } else if (this.position == 'right') { + elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px'); + elem.css('left', box.left + box.width - this.labelWidth + 'px'); + } + }; + + + CssTransformAxisLabel.prototype = new HtmlAxisLabel(); + CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; + function CssTransformAxisLabel(axisName, position, padding, plot, opts) { + HtmlAxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + CssTransformAxisLabel.prototype.calculateSize = function() { + HtmlAxisLabel.prototype.calculateSize.call(this); + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelHeight + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + var stransforms = { + '-moz-transform': '', + '-webkit-transform': '', + '-o-transform': '', + '-ms-transform': '' + }; + if (x != 0 || y != 0) { + var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; + stransforms['-moz-transform'] += stdTranslate; + stransforms['-webkit-transform'] += stdTranslate; + stransforms['-o-transform'] += stdTranslate; + stransforms['-ms-transform'] += stdTranslate; + } + if (degrees != 0) { + var rotation = degrees / 90; + var stdRotate = ' rotate(' + degrees + 'deg)'; + stransforms['-moz-transform'] += stdRotate; + stransforms['-webkit-transform'] += stdRotate; + stransforms['-o-transform'] += stdRotate; + stransforms['-ms-transform'] += stdRotate; + } + var s = 'top: 0; left: 0; '; + for (var prop in stransforms) { + if (stransforms[prop]) { + s += prop + ':' + stransforms[prop] + ';'; + } + } + s += ';'; + return s; + }; + + CssTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = { x: 0, y: 0, degrees: 0 }; + if (this.position == 'bottom') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top + box.height - this.labelHeight; + } else if (this.position == 'top') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top; + } else if (this.position == 'left') { + offsets.degrees = -90; + offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } else if (this.position == 'right') { + offsets.degrees = 90; + offsets.x = box.left + box.width - this.labelWidth/2 + - this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } + return offsets; + }; + + CssTransformAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); + var offsets = this.calculateOffsets(box); + var elem = $('
' + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + }; + + + IeTransformAxisLabel.prototype = new CssTransformAxisLabel(); + IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel; + function IeTransformAxisLabel(axisName, position, padding, plot, opts) { + CssTransformAxisLabel.prototype.constructor.call(this, axisName, + position, padding, + plot, opts); + this.requiresResize = false; + } + + IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + // I didn't feel like learning the crazy Matrix stuff, so this uses + // a combination of the rotation transform and CSS positioning. + var s = ''; + if (degrees != 0) { + var rotation = degrees/90; + while (rotation < 0) { + rotation += 4; + } + s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); '; + // see below + this.requiresResize = (this.position == 'right'); + } + if (x != 0) { + s += 'left: ' + x + 'px; '; + } + if (y != 0) { + s += 'top: ' + y + 'px; '; + } + return s; + }; + + IeTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call( + this, box); + // adjust some values to take into account differences between + // CSS and IE rotations. + if (this.position == 'top') { + // FIXME: not sure why, but placing this exactly at the top causes + // the top axis label to flip to the bottom... + offsets.y = box.top + 1; + } else if (this.position == 'left') { + offsets.x = box.left; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } else if (this.position == 'right') { + offsets.x = box.left + box.width - this.labelHeight; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } + return offsets; + }; + + IeTransformAxisLabel.prototype.draw = function(box) { + CssTransformAxisLabel.prototype.draw.call(this, box); + if (this.requiresResize) { + var elem = this.plot.getPlaceholder().find("." + this.axisName + "Label"); + // Since we used CSS positioning instead of transforms for + // translating the element, and since the positioning is done + // before any rotations, we have to reset the width and height + // in case the browser wrapped the text (specifically for the + // y2axis). + elem.css('width', this.labelWidth); + elem.css('height', this.labelHeight); + } + }; + + + function init(plot) { + // This is kind of a hack. There are no hooks in Flot between + // the creation and measuring of the ticks (setTicks, measureTickLabels + // in setupGrid() ) and the drawing of the ticks and plot box + // (insertAxisLabels in setupGrid() ). + // + // Therefore, we use a trick where we run the draw routine twice: + // the first time to get the tick measurements, so that we can change + // them, and then have it draw it again. + var secondPass = false; + + var axisLabels = {}; + var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 }; + + var defaultPadding = 2; // padding between axis and tick labels + plot.hooks.draw.push(function (plot, ctx) { + if (!secondPass) { + // MEASURE AND SET OPTIONS + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + if (!opts || !opts.axisLabel || !axis.show) + return; + + var renderer = null; + + if (!opts.axisLabelUseHtml && + navigator.appName == 'Microsoft Internet Explorer') { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1); + } + if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = CssTransformAxisLabel; + } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = IeTransformAxisLabel; + } else if (opts.axisLabelUseCanvas) { + renderer = CanvasAxisLabel; + } else { + renderer = HtmlAxisLabel; + } + } else { + if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) { + renderer = HtmlAxisLabel; + } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) { + renderer = CanvasAxisLabel; + } else { + renderer = CssTransformAxisLabel; + } + } + + var padding = opts.axisLabelPadding === undefined ? + defaultPadding : opts.axisLabelPadding; + + axisLabels[axisName] = new renderer(axisName, + axis.position, padding, + plot, opts); + + // flot interprets axis.labelHeight and .labelWidth as + // the height and width of the tick labels. We increase + // these values to make room for the axis label and + // padding. + + axisLabels[axisName].calculateSize(); + + // AxisLabel.height and .width are the size of the + // axis label and padding. + axis.labelHeight += axisLabels[axisName].height; + axis.labelWidth += axisLabels[axisName].width; + opts.labelHeight = axis.labelHeight; + opts.labelWidth = axis.labelWidth; + }); + // re-draw with new label widths and heights + secondPass = true; + plot.setupGrid(); + plot.draw(); + } else { + // DRAW + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + if (!opts || !opts.axisLabel || !axis.show) + return; + + axisLabels[axisName].draw(axis.box); + }); + } + }); + } + + + $.plot.plugins.push({ + init: init, + options: options, + name: 'axisLabels', + version: '2.0b0' + }); +})(jQuery); From 2045500c6e724300089d5ef98273533cf3951b3b Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 23:21:34 -0400 Subject: [PATCH 07/66] make external_auth table searchable in django admin; fix missed instance of login link which should have been behind DISABLE_LOGIN_BUTTON --- common/djangoapps/external_auth/admin.py | 6 +++++- lms/templates/portal/course_about.html | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/external_auth/admin.py b/common/djangoapps/external_auth/admin.py index 343492bca7..e93325bcb2 100644 --- a/common/djangoapps/external_auth/admin.py +++ b/common/djangoapps/external_auth/admin.py @@ -5,4 +5,8 @@ django admin pages for courseware model from external_auth.models import * from django.contrib import admin -admin.site.register(ExternalAuthMap) +class ExternalAuthMapAdmin(admin.ModelAdmin): + search_fields = ['external_id','user__username'] + date_hierarchy = 'dtcreated' + +admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin) diff --git a/lms/templates/portal/course_about.html b/lms/templates/portal/course_about.html index ac7b9090d0..8d2d45117d 100644 --- a/lms/templates/portal/course_about.html +++ b/lms/templates/portal/course_about.html @@ -76,7 +76,11 @@
%endif %else: - Register for ${course.number} + Log In +% endif + to enroll.'>Register for ${course.number} %endif From 31dd119e79fe3de694e3719302449a1272592155 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 17:50:11 -0400 Subject: [PATCH 08/66] settings.DATABASE_FOR_PSYCHOMETRICS overrides db for psychometrics --- .../management/commands/init_psychometrics.py | 8 ++++++-- lms/djangoapps/psychometrics/psychoanalyze.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index b7c9779d08..5e782df595 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -11,10 +11,14 @@ from track.models import * from psychometrics.models import * from xmodule.modulestore import Location +from django.conf import settings from django.core.management.base import BaseCommand #db = "ocwtutor" # for debugging -db = "default" +#db = "default" + +db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default') + class Command(BaseCommand): help = "initialize PsychometricData tables from StudentModule instances (and tracking data, if in SQL)." @@ -56,7 +60,7 @@ class Command(BaseCommand): tset = tset.filter(event_source='server') tset = tset.filter(event__contains="'%s'" % url) checktimes = [x.dtcreated for x in tset] - pmd.checktimes = json.dumps(checktimes) + pmd.checktimes = checktimes if not len(checktimes)==pmd.attempts: print "Oops, mismatch in number of attempts and check times for %s" % pmd diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index e8dd7b4684..c798c73609 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -12,6 +12,7 @@ import math import numpy as np from scipy.optimize import curve_fit +from django.conf import settings from django.db.models import Sum, Max from psychometrics.models import * from xmodule.modulestore import Location @@ -19,7 +20,9 @@ from xmodule.modulestore import Location log = logging.getLogger("mitx.psychometrics") #db = "ocwtutor" # for debugging -db = "default" +#db = "default" + +db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default') #----------------------------------------------------------------------------- # fit functions From ccbbb5a99388ace2f1295cb46125bb4a095e4f4f Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 19:39:11 -0400 Subject: [PATCH 09/66] commit_id may be specified for modulestore reload - for multi-worker configs also show pid in migration and instructor dashboard views for thread debugging --- lms/djangoapps/instructor/views.py | 1 + lms/djangoapps/lms_migration/migrate.py | 38 ++++++++++++++++--- .../courseware/instructor_dashboard.html | 2 + lms/urls.py | 1 + 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 74f186b689..f78a1160de 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -225,6 +225,7 @@ def instructor_dashboard(request, course_id): 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), + 'djangopid' : os.getpid(), } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index a3a1c595be..403063f983 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -35,7 +35,17 @@ def getip(request): ip = request.META.get('REMOTE_ADDR','None') return ip -def manage_modulestores(request,reload_dir=None): + +def get_commit_id(course): + return course.metadata.get('GIT_COMMIT_ID','No commit id') + # getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id') + + +def set_commit_id(course,commit_id): + course.metadata['GIT_COMMIT_ID'] = commit_id + # setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id) + +def manage_modulestores(request, reload_dir=None, commit_id=None): ''' Manage the static in-memory modulestores. @@ -52,8 +62,9 @@ def manage_modulestores(request,reload_dir=None): ip = getip(request) if LOCAL_DEBUG: - html += '

IP address: %s ' % ip - html += '

User: %s ' % request.user + html += '

IP address: %s

' % ip + html += '

User: %s

' % request.user + html += '

My pid: %s

' % os.getpid() log.debug('request from ip=%s, user=%s' % (ip,request.user)) if not (ip in ALLOWED_IPS or 'any' in ALLOWED_IPS): @@ -66,14 +77,27 @@ def manage_modulestores(request,reload_dir=None): return HttpResponse(html, status=403) #---------------------------------------- - # reload course if specified + # reload course if specified; handle optional commit_id if reload_dir is not None: if reload_dir not in def_ms.courses: html += '

Error: "%s" is not a valid course directory

' % reload_dir else: - html += '

Reloaded course directory "%s"

' % reload_dir - def_ms.try_load_course(reload_dir) + # reloading based on commit_id is needed when running mutiple worker threads, + # so that a given thread doesn't reload the same commit multiple times + current_commit_id = get_commit_id(def_ms.courses[reload_dir]) + log.debug('commit_id="%s"' % commit_id) + log.debug('current_commit_id="%s"' % current_commit_id) + + if (commit_id is not None) and (commit_id==current_commit_id): + html += "

Already at commit id %s for %s

" % (commit_id, reload_dir) + else: + html += '

Reloaded course directory "%s"

' % reload_dir + def_ms.try_load_course(reload_dir) + gdir = settings.DATA_DIR / reload_dir + new_commit_id = os.popen('cd %s; git log -n 1 | head -1' % gdir).read().strip().split(' ')[1] + set_commit_id(def_ms.courses[reload_dir], new_commit_id) + html += '

commit_id=%s

' % new_commit_id #---------------------------------------- @@ -94,6 +118,8 @@ def manage_modulestores(request,reload_dir=None): html += '
' html += '

Course: %s (%s)

' % (course.display_name,cdir) + html += '

commit_id=%s

' % get_commit_id(course) + for field in dumpfields: data = getattr(course,field) html += '

%s

' % field diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index d73eda1ed7..e822f05f92 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -59,6 +59,8 @@ function goto( mode) Admin ] +
${djangopid}
+
diff --git a/lms/urls.py b/lms/urls.py index 8484ccc40b..49febaf84e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -237,6 +237,7 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): urlpatterns += ( url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'), url(r'^migrate/reload/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'), + url(r'^migrate/reload/(?P[^/]+)/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'), url(r'^gitreload$', 'lms_migration.migrate.gitreload'), url(r'^gitreload/(?P[^/]+)$', 'lms_migration.migrate.gitreload'), ) From 2f2e71e0d12c69c1f652ab65ee13ebc702ae2535 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 21:08:31 -0400 Subject: [PATCH 10/66] add tracking log entries for modulestore reload --- lms/djangoapps/lms_migration/migrate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index 403063f983..ecde31d6dd 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -91,6 +91,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): if (commit_id is not None) and (commit_id==current_commit_id): html += "

Already at commit id %s for %s

" % (commit_id, reload_dir) + track.views.server_track(request, + 'reload %s skipped already at %s (pid=%s)' % (reload_dir, + commit_id, + os.getpid(), + ), + {}, page='migrate') else: html += '

Reloaded course directory "%s"

' % reload_dir def_ms.try_load_course(reload_dir) @@ -98,6 +104,9 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): new_commit_id = os.popen('cd %s; git log -n 1 | head -1' % gdir).read().strip().split(' ')[1] set_commit_id(def_ms.courses[reload_dir], new_commit_id) html += '

commit_id=%s

' % new_commit_id + track.views.server_track(request, 'reloaded %s now at %s (pid=%s)' % (reload_dir, + new_commit_id, + os.getpid()), {}, page='migrate') #---------------------------------------- From b501367ed3b6bf01cb3f49bd1d19a27ac049fba0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 28 Aug 2012 10:32:15 -0400 Subject: [PATCH 11/66] make the test ajax_url format consistent with real code * specifically, no trailing slash --- common/lib/xmodule/xmodule/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 4103a7373e..654b6beb15 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -29,7 +29,7 @@ from nose.plugins.skip import SkipTest from mock import Mock i4xs = ModuleSystem( - ajax_url='/', + ajax_url='courses/course_id/modx/a_location', track_function=Mock(), get_module=Mock(), render_template=Mock(), From c1d92f9351de610984fd83ab445a9927ca66caed Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 16:57:47 -0400 Subject: [PATCH 12/66] track psychometrics requests; add grade statistics, check for wrong value of max_grade (something not right about StudentModule.max_grade) --- lms/djangoapps/instructor/views.py | 1 + lms/djangoapps/psychometrics/psychoanalyze.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f78a1160de..d812791c3d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -209,6 +209,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg + track.views.server_track(request, 'psychometrics %s' % problem, {}, page='idashboard') if idash_mode=='Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index c798c73609..bb2a6ba6a8 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -146,6 +146,13 @@ def generate_plots_for_problem(problem): xdat = range(1,max_attempts+1) dataset = {'xdat': xdat} + # compute grade statistics + grades = [pmd.studentmodule.grade for pmd in pmdset] + gsv = StatVar() + for g in grades: + gsv += g + msg += "

Grade distribution: %s

" % gsv + # generate grade histogram ghist = [] @@ -159,8 +166,12 @@ def generate_plots_for_problem(problem): }] }""" + if gsv.max > max_grade: + msg += "

Something is wrong: max_grade=%s, but max(grades)=%s

" % (max_grade, gsv.max) + max_grade = gsv.max + if max_grade > 1: - ghist = make_histogram([pmd.studentmodule.grade for pmd in pmdset],np.linspace(0,max_grade,max_grade+1)) + ghist = make_histogram(grades, np.linspace(0,max_grade,max_grade+1)) ghist_json = json.dumps(ghist.items()) plot = {'title': "Grade histogram for %s" % problem, @@ -192,7 +203,7 @@ def generate_plots_for_problem(problem): dtsv += dt ct0 = ct if dtsv.cnt > 2: - msg += "
time differences between checks: %s" % dtsv + msg += "

Time differences between checks: %s

" % dtsv bins = np.linspace(0,1.5*dtsv.sdv(),30) dbar = bins[1]-bins[0] thist = make_histogram(dtset,bins) @@ -223,7 +234,7 @@ def generate_plots_for_problem(problem): ylast = y + ylast yset['ydat'] = ydat - if len(ydat)>5: # try to fit to logistic function if enough data points + if len(ydat)>3: # try to fit to logistic function if enough data points cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts/2.0]) yset['fitparam'] = cfp yset['fitpts'] = func_2pl(np.array(xdat),*cfp[0]) From 9266bcca6d4e6a0e5af10f227a62d122a7bfa6e0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 10 Sep 2012 17:42:01 -0400 Subject: [PATCH 13/66] fix stupid bug in textbook handling code --- common/lib/xmodule/xmodule/course_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 4a009dcae4..7aa904205d 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -106,7 +106,7 @@ class CourseDescriptor(SequenceDescriptor): # the rest of the courseware. log.exception("Couldn't load textbook") continue - textbooks.append() + textbooks.append(txt) xml_object.remove(textbook) #Load the wiki tag if it exists From e62e58dc6eae4eb32e4730643c721ba90329cd53 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 20:33:56 -0400 Subject: [PATCH 14/66] log attempts in tracking --- common/lib/xmodule/xmodule/capa_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 8e2d12d5e9..8bf1a56404 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -507,6 +507,7 @@ class CapaModule(XModule): # 'success' will always be incorrect event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success + event_info['attempts'] = self.attempts self.system.track_function('save_problem_check', event_info) if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback From 97a32e64eef6fcfca93876f04580fed8207e877f Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:33:54 -0400 Subject: [PATCH 15/66] fix tooltip titles in seq_module for non-new-xml-format courses --- common/lib/xmodule/xmodule/seq_module.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 65f692957c..26bdd286c3 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -75,7 +75,7 @@ class SequenceModule(XModule): contents = [] for child in self.get_display_items(): progress = child.get_progress() - contents.append({ + childinfo = { 'content': child.get_html(), 'title': "\n".join( grand_child.display_name.strip() @@ -85,7 +85,10 @@ class SequenceModule(XModule): 'progress_status': Progress.to_js_status_str(progress), 'progress_detail': Progress.to_js_detail_str(progress), 'type': child.get_icon_class(), - }) + } + if childinfo['title']=='': + childinfo['title'] = child.metadata['display_name'] + contents.append(childinfo) params = {'items': contents, 'element_id': self.location.html_id(), From d8c8c85041b30a95ef16272235a242fca787fc71 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:42:44 -0400 Subject: [PATCH 16/66] tabs to spaces --- common/lib/xmodule/xmodule/seq_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 26bdd286c3..e7e97626db 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -75,7 +75,7 @@ class SequenceModule(XModule): contents = [] for child in self.get_display_items(): progress = child.get_progress() - childinfo = { + childinfo = { 'content': child.get_html(), 'title': "\n".join( grand_child.display_name.strip() @@ -86,7 +86,7 @@ class SequenceModule(XModule): 'progress_detail': Progress.to_js_detail_str(progress), 'type': child.get_icon_class(), } - if childinfo['title']=='': + if childinfo['title']=='': childinfo['title'] = child.metadata['display_name'] contents.append(childinfo) From c6f794547fd77dd99fa9ad8b64ada3fdb9bfbb61 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:45:38 -0400 Subject: [PATCH 17/66] more tabs -> spaces --- common/lib/xmodule/xmodule/seq_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index e7e97626db..607c1e500e 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -87,7 +87,7 @@ class SequenceModule(XModule): 'type': child.get_icon_class(), } if childinfo['title']=='': - childinfo['title'] = child.metadata['display_name'] + childinfo['title'] = child.metadata['display_name'] contents.append(childinfo) params = {'items': contents, From 56d44ec7f39fe47cebb1c6e50f555e7cb259b2a4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:55:58 -0400 Subject: [PATCH 18/66] display_name may be empty (eg in tests) --- common/lib/xmodule/xmodule/seq_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 607c1e500e..b05ea36e50 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -87,7 +87,7 @@ class SequenceModule(XModule): 'type': child.get_icon_class(), } if childinfo['title']=='': - childinfo['title'] = child.metadata['display_name'] + childinfo['title'] = child.metadata.get('display_name','') contents.append(childinfo) params = {'items': contents, From bffd9ac38d799e7adb447c018391c2734bbe9840 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 22:28:37 -0400 Subject: [PATCH 19/66] center histogram bars; pep8 --- lms/djangoapps/psychometrics/psychoanalyze.py | 120 ++++++++++-------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index bb2a6ba6a8..dd7d328278 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -1,7 +1,7 @@ # # File: psychometrics/psychoanalyze.py # -# generate pyschometrics plots from PsychometricData +# generate pyschometrics plots from PsychometricData from __future__ import division @@ -19,98 +19,108 @@ from xmodule.modulestore import Location log = logging.getLogger("mitx.psychometrics") -#db = "ocwtutor" # for debugging +#db = "ocwtutor" # for debugging #db = "default" -db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default') +db = getattr(settings, 'DATABASE_FOR_PSYCHOMETRICS', 'default') #----------------------------------------------------------------------------- # fit functions -def func_2pl(x,a,b): + +def func_2pl(x, a, b): """ 2-parameter logistic function """ D = 1.7 - edax = np.exp(D*a*(x-b)) - return edax / (1+edax) + edax = np.exp(D * a * (x - b)) + return edax / (1 + edax) #----------------------------------------------------------------------------- # statistics class + class StatVar(object): """ Simple statistics on floating point numbers: avg, sdv, var, min, max """ - def __init__(self,unit=1): + def __init__(self, unit=1): self.sum = 0 self.sum2 = 0 self.cnt = 0 self.unit = unit self.min = None self.max = None - def add(self,x): + + def add(self, x): if x is None: return if self.min is None: self.min = x else: - if xself.max: + if x > self.max: self.max = x self.sum += x self.sum2 += x**2 self.cnt += 1 + def avg(self): if self.cnt is None: return 0 return self.sum / 1.0 / self.cnt / self.unit + def var(self): if self.cnt is None: return 0 return (self.sum2 / 1.0 / self.cnt / (self.unit**2)) - (self.avg()**2) + def sdv(self): v = self.var() if v>0: return math.sqrt(v) else: return 0 + def __str__(self): - return 'cnt=%d, avg=%f, sdv=%f' % (self.cnt,self.avg(),self.sdv()) - def __add__(self,x): + return 'cnt=%d, avg=%f, sdv=%f' % (self.cnt, self.avg(), self.sdv()) + + def __add__(self, x): self.add(x) return self #----------------------------------------------------------------------------- # histogram generator -def make_histogram(ydata,bins=None): + +def make_histogram(ydata, bins=None): ''' Generate histogram of ydata using bins provided, or by default bins from 0 to 100 by 10. bins should be ordered in increasing order. - + returns dict with keys being bins, and values being counts. special: hist['bins'] = bins ''' if bins is None: - bins = range(0,100,10) - + bins = range(0, 100, 10) + nbins = len(bins) - hist = dict(zip(bins,[0] * nbins)) + hist = dict(zip(bins, [0] * nbins)) for y in ydata: - for b in bins[::-1]: # in reverse order + for b in bins[::-1]: # in reverse order if y>b: hist[b] += 1 break # hist['bins'] = bins return hist - + #----------------------------------------------------------------------------- + def problems_with_psychometric_data(course_id): ''' Return dict of {problems (location urls): count} for which psychometric data is available. @@ -118,36 +128,37 @@ def problems_with_psychometric_data(course_id): ''' pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id) plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()] - problems = dict( (p,pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist ) + problems = dict( (p, pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist ) return problems #----------------------------------------------------------------------------- + def generate_plots_for_problem(problem): - + pmdset = PsychometricData.objects.using(db).filter(studentmodule__module_state_key=problem) nstudents = pmdset.count() msg = "" plots = [] if nstudents < 2: - msg += "%s nstudents=%d --> skipping, too few" % (problem,nstudents) + msg += "%s nstudents=%d --> skipping, too few" % (problem, nstudents) return msg, plots max_grade = pmdset[0].studentmodule.max_grade agdat = pmdset.aggregate(Sum('attempts'), Max('attempts')) max_attempts = agdat['attempts__max'] - total_attempts = agdat['attempts__sum'] # not used yet + total_attempts = agdat['attempts__sum'] # not used yet msg += "max attempts = %d" % max_attempts - xdat = range(1,max_attempts+1) + xdat = range(1, max_attempts + 1) dataset = {'xdat': xdat} # compute grade statistics - grades = [pmd.studentmodule.grade for pmd in pmdset] + grades = [pmd.studentmodule.grade for pmd in pmdset] gsv = StatVar() for g in grades: gsv += g @@ -171,14 +182,14 @@ def generate_plots_for_problem(problem): max_grade = gsv.max if max_grade > 1: - ghist = make_histogram(grades, np.linspace(0,max_grade,max_grade+1)) + ghist = make_histogram(grades, np.linspace(0, max_grade, max_grade + 1)) ghist_json = json.dumps(ghist.items()) plot = {'title': "Grade histogram for %s" % problem, 'id': 'histogram', 'info': '', 'data': "var dhist = %s;\n" % ghist_json, - 'cmd': "[ {data: dhist, bars: { show: true }} ], %s" % axisopts, + 'cmd': '[ {data: dhist, bars: { show: true, align: "center" }} ], %s' % axisopts, } plots.append(plot) else: @@ -186,27 +197,27 @@ def generate_plots_for_problem(problem): # histogram of time differences between checks # Warning: this is inefficient - doesn't scale to large numbers of students - dtset = [] # time differences in minutes + dtset = [] # time differences in minutes dtsv = StatVar() for pmd in pmdset: try: - checktimes = eval(pmd.checktimes) # update log of attempt timestamps + checktimes = eval(pmd.checktimes) # update log of attempt timestamps except: continue - if len(checktimes)<2: + if len(checktimes) < 2: continue ct0 = checktimes[0] for ct in checktimes[1:]: - dt = (ct-ct0).total_seconds()/60.0 - if dt<20: # ignore if dt too long + dt = (ct - ct0).total_seconds() / 60.0 + if dt < 20: # ignore if dt too long dtset.append(dt) dtsv += dt ct0 = ct if dtsv.cnt > 2: msg += "

Time differences between checks: %s

" % dtsv - bins = np.linspace(0,1.5*dtsv.sdv(),30) - dbar = bins[1]-bins[0] - thist = make_histogram(dtset,bins) + bins = np.linspace(0, 1.5 * dtsv.sdv(), 30) + dbar = bins[1] - bins[0] + thist = make_histogram(dtset, bins) thist_json = json.dumps(sorted(thist.items(), key=lambda(x): x[0])) axisopts = """{ xaxes: [{ axisLabel: 'Time (min)'}], yaxes: [{position: 'left',axisLabel: 'Count'}]}""" @@ -215,33 +226,33 @@ def generate_plots_for_problem(problem): 'id': 'thistogram', 'info': '', 'data': "var thist = %s;\n" % thist_json, - 'cmd': "[ {data: thist, bars: { show: true, barWidth:%f }} ], %s" % (dbar, axisopts), + 'cmd': '[ {data: thist, bars: { show: true, align: "center", barWidth:%f }} ], %s' % (dbar, axisopts), } plots.append(plot) # one IRT plot curve for each grade received (TODO: this assumes integer grades) - for grade in range(1,int(max_grade)+1): + for grade in range(1, int(max_grade) + 1): yset = {} gset = pmdset.filter(studentmodule__grade=grade) ngset = gset.count() - if ngset==0: + if ngset == 0: continue ydat = [] ylast = 0 for x in xdat: - y = gset.filter(attempts=x).count()/ngset + y = gset.filter(attempts=x).count() / ngset ydat.append( y + ylast ) ylast = y + ylast yset['ydat'] = ydat - if len(ydat)>3: # try to fit to logistic function if enough data points - cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts/2.0]) + if len(ydat) > 3: # try to fit to logistic function if enough data points + cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0]) yset['fitparam'] = cfp - yset['fitpts'] = func_2pl(np.array(xdat),*cfp[0]) - yset['fiterr'] = [yd-yf for (yd,yf) in zip(ydat,yset['fitpts'])] - fitx = np.linspace(xdat[0],xdat[-1],100) + yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0]) + yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])] + fitx = np.linspace(xdat[0], xdat[-1], 100) yset['fitx'] = fitx - yset['fity'] = func_2pl(np.array(fitx),*cfp[0]) + yset['fity'] = func_2pl(np.array(fitx), *cfp[0]) dataset['grade_%d' % grade] = yset @@ -257,27 +268,27 @@ def generate_plots_for_problem(problem): }""" # generate points for flot plot - for grade in range(1,int(max_grade)+1): + for grade in range(1, int(max_grade) + 1): jsdata = "" jsplots = [] gkey = 'grade_%d' % grade if gkey in dataset: yset = dataset[gkey] - jsdata += "var d%d = %s;\n" % (grade,json.dumps(zip(xdat,yset['ydat']))) + jsdata += "var d%d = %s;\n" % (grade, json.dumps(zip(xdat, yset['ydat']))) jsplots.append('{ data: d%d, lines: { show: false }, points: { show: true}, color: "red" }' % grade) if 'fitpts' in yset: - jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'],yset['fity']))) + jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'], yset['fity']))) jsplots.append('{ data: fit, lines: { show: true }, color: "blue" }') - (a,b) = yset['fitparam'][0] - irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a,b) + (a, b) = yset['fitparam'][0] + irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a, b) else: irtinfo = "" - plots.append({'title': 'IRT Plot for grade=%s %s' % (grade,irtinfo), + plots.append({'title': 'IRT Plot for grade=%s %s' % (grade, irtinfo), 'id': "irt%s" % grade, 'info': '', 'data': jsdata, - 'cmd' : '[%s], %s' % (','.join(jsplots), axisopts), + 'cmd': '[%s], %s' % (','.join(jsplots), axisopts), }) #log.debug('plots = %s' % plots) @@ -285,6 +296,7 @@ def generate_plots_for_problem(problem): #----------------------------------------------------------------------------- + def make_psychometrics_data_update_handler(studentmodule): """ Construct and return a procedure which may be called to update @@ -307,13 +319,13 @@ def make_psychometrics_data_update_handler(studentmodule): state = json.loads(sm.state) done = state['done'] except: - log.exception("Oops, failed to eval state for %s (state=%s)" % (sm,sm.state)) + log.exception("Oops, failed to eval state for %s (state=%s)" % (sm, sm.state)) return pmd.done = done pmd.attempts = state['attempts'] try: - checktimes = eval(pmd.checktimes) # update log of attempt timestamps + checktimes = eval(pmd.checktimes) # update log of attempt timestamps except: checktimes = [] checktimes.append(datetime.datetime.now()) From 3fd640aef326f96241384e339f79f767748d5db1 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 10:22:03 -0400 Subject: [PATCH 20/66] Removed display none for histogram --- lms/static/sass/course/courseware/_courseware.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 0532f04b42..863bcad139 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -80,7 +80,6 @@ div.course-wrapper { } .histogram { - display: none; width: 200px; height: 150px; } From 064b5e793252c2afb7cc2f2a1722ab0f26f9ca55 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 11:25:54 -0400 Subject: [PATCH 21/66] Remove widows in accordion header --- lms/templates/courseware/courseware.html | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 29b9be3789..a433ddc9fc 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -12,6 +12,7 @@ + ## codemirror @@ -21,6 +22,7 @@ ## ## + <%static:js group='courseware'/> <%static:js group='discussion'/> @@ -35,6 +37,22 @@ From 4732e39cb4d0da12c28f6078bb950a089798ffb0 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 12:09:42 -0400 Subject: [PATCH 22/66] info-title margin fixed --- lms/static/sass/course/_info.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index e25bb9d8c4..80db054afd 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -25,11 +25,6 @@ div.info-wrapper { margin-bottom: lh(); padding-bottom: lh(.5); - &:first-child { - margin: 0 (-(lh(.5))) lh(); - padding: lh(.5); - } - ol, ul { margin: 0; list-style-type: disk; From 7a248f0154bdba45874dba0cec5c6b4baa2f3b1f Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 14:05:07 -0400 Subject: [PATCH 23/66] Added fixes to some style and one Cale fix to get things working properly --- cms/djangoapps/contentstore/views.py | 2 +- cms/static/sass/_base.scss | 2 ++ cms/static/sass/_calendar.scss | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 490f49a41c..d701db33a3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -53,7 +53,7 @@ def index(request): """ courses = modulestore().get_items(['i4x', None, None, 'course', None]) return render_to_response('index.html', { - 'courses': [(course.metadata['display_name'], + 'courses': [(course.metadata.get('display_name'), reverse('course_index', args=[ course.location.org, course.location.course, diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 410f74ee07..90a9629351 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -14,9 +14,11 @@ $yellow: #fff8af; $cream: #F6EFD4; $border-color: #ddd; + // edX colors $blue: rgb(29,157,217); $pink: rgb(182,37,104); +$error-red: rgb(253, 87, 87); @mixin hide-text { background-color: transparent; diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index 35609b2d56..4c007bb561 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -330,11 +330,6 @@ section.cal { &:hover { opacity: 1; - width: flex-grid(5) + flex-gutter(); - - + section.main-content { - width: flex-grid(7); - } } > header { From f35412f3f4e3fd8c048632db83b277014b41f011 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 11 Sep 2012 16:29:17 -0400 Subject: [PATCH 24/66] Don't bind update_schematics to global window.onload --- common/lib/xmodule/xmodule/js/src/capa/schematic.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js index e07b98d63c..b01f6e12e8 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js +++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js @@ -2023,7 +2023,16 @@ function add_schematic_handler(other_onload) { update_schematics(); } } -window.onload = add_schematic_handler(window.onload); +/* + * THK: Attaching update_schematic to window.onload is rather presumptuous... + * The function is called for EVERY page load, whether in courseware or in + * course info, in 6.002x or the public health course. It is also redundant + * because courseware includes an explicit call to update_schematic after + * each ajax exchange. In this case, calling update_schematic twice appears + * to contribute to a bug in Firefox that does not render the schematic + * properly depending on timing. + */ +//window.onload = add_schematic_handler(window.onload); // ask each schematic input widget to update its value field for submission function prepare_schematics() { From 5e4a498cfe0745396b2f2a88c3c310a97e8bd86f Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 11 Sep 2012 15:09:49 -0700 Subject: [PATCH 25/66] Provide a reasonable seeding mechanism for jsresponses --- common/lib/capa/capa/javascript_problem_generator.js | 4 +--- common/lib/capa/capa/responsetypes.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/javascript_problem_generator.js b/common/lib/capa/capa/javascript_problem_generator.js index 8c8d39b19f..1cd4616c5a 100644 --- a/common/lib/capa/capa/javascript_problem_generator.js +++ b/common/lib/capa/capa/javascript_problem_generator.js @@ -11,13 +11,11 @@ importAll("xproblem"); generatorModulePath = process.argv[2]; dependencies = JSON.parse(process.argv[3]); -seed = process.argv[4]; +seed = JSON.parse(process.argv[4]); params = JSON.parse(process.argv[5]); if(seed==null){ seed = 4; -}else{ - seed = parseInt(seed); } for(var i = 0; i < dependencies.length; i++){ diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b803452b8c..d9216f06d6 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -408,7 +408,7 @@ class JavascriptResponse(LoncapaResponse): output = self.call_node([generator_file, self.generator, json.dumps(self.generator_dependencies), - json.dumps(str(self.system.seed)), + json.dumps(self.context['random'].getrandbits(9)), json.dumps(self.params)]).strip() return json.loads(output) From bd5fc64462449fe0085c931bb75e1227a13cd587 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 11 Sep 2012 18:08:27 -0700 Subject: [PATCH 26/66] make seeds consistent with other responsetypes --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d9216f06d6..7f1ff32f67 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -408,7 +408,7 @@ class JavascriptResponse(LoncapaResponse): output = self.call_node([generator_file, self.generator, json.dumps(self.generator_dependencies), - json.dumps(self.context['random'].getrandbits(9)), + json.dumps(str(self.context['the_lcp'].seed)), json.dumps(self.params)]).strip() return json.loads(output) From 05c3e028c2d9b37587de4080a549ad1b72d787f7 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 09:32:24 -0400 Subject: [PATCH 27/66] Added new press releases --- .../images/press/baltsun_logo_178x138.jpg | Bin 0 -> 3779 bytes .../images/press/bostinno_logo_178x138.jpg | Bin 0 -> 3865 bytes .../images/press/bostonmag_logo_178x138.jpg | Bin 0 -> 3441 bytes .../images/press/csmonitor_logo_178x138.jpg | Bin 0 -> 5130 bytes .../press/insidehighered_logo_178x138.jpg | Bin 0 -> 5268 bytes .../images/press/itbriefing_logo_178x138.jpg | Bin 0 -> 2904 bytes .../images/press/radioboston_logo_178x138.jpg | Bin 0 -> 5558 bytes .../images/press/smartplanet_logo_178x138.jpg | Bin 0 -> 2855 bytes .../images/press/techreview_logo_178x138.jpg | Bin 0 -> 4261 bytes .../images/press/thetech_logo_178x138.jpg | Bin 0 -> 3958 bytes lms/static/images/press/time_logo_178x138.jpg | Bin 0 -> 4067 bytes lms/templates/press.json | 183 ++++++++++++++++++ lms/templates/static_templates/press.html | 1 - 13 files changed, 183 insertions(+), 1 deletion(-) create mode 100755 lms/static/images/press/baltsun_logo_178x138.jpg create mode 100644 lms/static/images/press/bostinno_logo_178x138.jpg create mode 100755 lms/static/images/press/bostonmag_logo_178x138.jpg create mode 100755 lms/static/images/press/csmonitor_logo_178x138.jpg create mode 100755 lms/static/images/press/insidehighered_logo_178x138.jpg create mode 100755 lms/static/images/press/itbriefing_logo_178x138.jpg create mode 100755 lms/static/images/press/radioboston_logo_178x138.jpg create mode 100755 lms/static/images/press/smartplanet_logo_178x138.jpg create mode 100755 lms/static/images/press/techreview_logo_178x138.jpg create mode 100755 lms/static/images/press/thetech_logo_178x138.jpg create mode 100755 lms/static/images/press/time_logo_178x138.jpg diff --git a/lms/static/images/press/baltsun_logo_178x138.jpg b/lms/static/images/press/baltsun_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..3dc619ac7b3caba28efcb41da9d0c22db1461106 GIT binary patch literal 3779 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 Df5!ho literal 0 HcmV?d00001 diff --git a/lms/static/images/press/bostonmag_logo_178x138.jpg b/lms/static/images/press/bostonmag_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..aa4f8a357718b71544654bf419351a867dde4dc8 GIT binary patch literal 3441 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 OudU$!#pTY!p8N;7J~1c& literal 0 HcmV?d00001 diff --git a/lms/static/images/press/csmonitor_logo_178x138.jpg b/lms/static/images/press/csmonitor_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..d389b7a6f11c6eea545de1f0abac49a86c8f0b68 GIT binary patch literal 5130 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 A!vFvP literal 0 HcmV?d00001 diff --git a/lms/static/images/press/insidehighered_logo_178x138.jpg b/lms/static/images/press/insidehighered_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..ad23f11a963b5ad4064b081964dd1a5256fcb38a GIT binary patch literal 5268 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 G8T&VQH1uu& literal 0 HcmV?d00001 diff --git a/lms/static/images/press/radioboston_logo_178x138.jpg b/lms/static/images/press/radioboston_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..e29949a31f5d718da0c097b9727d18956ab5167b GIT binary patch literal 5558 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 Ctr9x` literal 0 HcmV?d00001 diff --git a/lms/static/images/press/time_logo_178x138.jpg b/lms/static/images/press/time_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..5d884fe0fe58b875b9c498dabcfd7d30c2bab884 GIT binary patch literal 4067 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

_zo2qY58Um@o`X6W* zOSc<(-I&KtzeX364g?dn5tb%4s^R5}(K6G~2%4VxWZog6?XSI{0|`AisO!}WrWgNu z%&Al>6dwIIv=aEAw371=t+0R~zeQTM9#To#W$mgOK5fN&!GE{fKTpa!K1ipH*28U* zE``XH?_=?yfZ#~LmOh0|CRAiLAeGE#P-cQrWkcmR6obQN+SM@( zbW?XWPpK9EfPs~(ndZVJ6*z<@^k+$j_=@@9#}JXYl&#(J)EH1x1=b&JT-S<5FTz5z z&aziM8AQ4u=it`U>9D08KzWhhU=JlkcBaK%OA+Pd(cdY#je>@~?OJrdP0as3PURTR zD&s^BYr&%|TDqDFyH>`UsU~e_Y9?^oJ7mc>==x)TOlYB)YgNY=bSx??VwLXL_`W%+ z<3+lNy(}%$G>{WjQT&DYM^Tei1Ss=R|9MZ8-uZZkgc?lpb|DC^lRciI55Z*%mLIq2g6V5JbbgD} z*{>h+e{M7qr(Vr6m*hHKiSYt7Wa$wTFMv zbx7fBn{%p<6X8(y#m6T~<-gm)>Z^B7GPQ0CEI1eB6HSktha(2pL`Fsc%c4w!<#0w$ z1ZO_lrPOXTD|4&&igSSFbJ|}#pA8-`!u1-jNExAq#;4)Z<4}i~CntZfYpfrbc3w`d zG#p7Na``RO@PiN4qh8*KxEFkVKNd}|Q7VB(Ntsj*^&<0{r;v*tshnZq_t;Ls>kES> zyKGR=bW;{O9gV{bH<33|y~~`0H|4N|#)`8;Ci^~cRH_K3gpc{<}6g25bx2>V(M z?mdNvMPLn@s!R}u9#UO4H3f82R>&_WJY)l&ylZ}zJNwE#6U*Pj{lGoeia5+1TPjV0 zM|1VfX5W$X_edOD%~8B9)q8=V6eiC1?v{|5t;5#n)XW1yg`qIa&_$=5!cP#YaItKu eWzWH7OnAzGhFVFN{Cx#=f6?gwlZEYg>c0TM(SDf# literal 0 HcmV?d00001 diff --git a/lms/templates/press.json b/lms/templates/press.json index 16942f06fb..f523f430af 100644 --- a/lms/templates/press.json +++ b/lms/templates/press.json @@ -1,4 +1,187 @@ [ + + { + "title": "Is MIT Giving Away the Farm?", + "url": "http://www.technologyreview.com/mitnews/428698/is-mit-giving-away-the-farm/", + "author": "Larry Hardesty", + "image": "techreview_logo_178x138.jpg", + "deck": "The surprising logic of MIT's free online education program.", + "publication": "Technology Review", + "publish_date": "September/October 2012" + }, + { + "title": "School’s Out, Forever", + "url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/", + "author": "Chris Vogel", + "image": "bostonmag_logo_178x138.jpg", + "deck": "A new online education program from Harvard and MIT is poised to transform what it means to go to college.", + "publication": "Boston Magazine", + "publish_date": "September 2012" + }, + { + "title": "Q&A: Anant Agarwal, edX’s president and first professor", + "url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/", + "author": " Molly Petrilla ", + "image": "smartplanet_logo_178x138.jpg", + "deck": "", + "publication": "Smart Planet", + "publish_date": "September 3, 2012" + }, + + { + "title": "EdX To Offer Proctored Final Exam For One Course", + "url": "http://www.thecrimson.com/article/2012/9/7/edx-offer-proctored-exams/", + "author": "Samuel Y. Weinstock", + "image": "harvardcrimson_logo_178x138.jpeg", + "deck": "", + "publication": "Harvard Crimson", + "publish_date": "September 7, 2012" + }, + { + "title": "MOOCing On Site", + "url": "http://www.insidehighered.com/news/2012/09/07/site-based-testing-deals-strengthen-case-granting-credit-mooc-students", + "author": "Steve Kolowich ", + "image": "insidehighered_logo_178x138.jpg", + "deck": "", + "publication": "Inside Higher Education", + "publish_date": "September 7, 2012" + }, + { + "title": "edX Curbs the Downfalls of Online Education By Announcing Supervised Final Exams", + "url": "http://bostinno.com/2012/09/07/edx-pearson-proctored-exams/", + "author": "Lauren Landry", + "image": "bostinno_logo_178x138.jpg", + "deck": "", + "publication": "Bostinno", + "publish_date": "September 7, 2012" + }, + { + "title": "Harvard and MIT online courses get 'real world' exams", + "url": "http://www.bbc.co.uk/news/education-19505776", + "author": "Sean Coughlan", + "image": "bbc_logo_178x138.jpeg", + "deck": "", + "publication": "BBC", + "publish_date": "September 6, 2012" + }, + { + "title": "Harvard-MIT Online School EdX to Offer Supervised Final Exams", + "url": "http://www.businessweek.com/news/2012-09-06/harvard-mit-online-school-edx-to-offer-supervised-final-exams", + "author": "Oliver Staley", + "image": "bloomberg_logo_178x138.jpeg", + "deck": "", + "publication": "Bloomberg Business Week", + "publish_date": "September 6, 2012" + }, + { + "title": "Colorado State to Offer Credits for Online Class", + "url": "http://www.nytimes.com/2012/09/07/education/colorado-state-to-offer-credits-for-online-class.html?_r=3", + "author": "Tamar Lewin", + "image": "nyt_logo_178x138.jpeg", + "deck": "", + "publication": "New York Times", + "publish_date": "September 6, 2012" + }, + { + "title": "edX Offers Proctored Exams for Open Online Course", + "url": "http://chronicle.com/blogs/wiredcampus/edx-offers-proctored-exams-for-open-online-course/39656", + "author": " Marc Parry", + "image": "chroniclehighered_logo_178x138.jpeg", + "deck": "", + "publication": "Chronicle of Higher Education", + "publish_date": "September 6, 2012" + }, + { + "title": "edX Offers Proctored Exams for Open Online Course", + "url": "http://itbriefing.net/modules.php?op=modload&name=News&file=article&sid=323229&newlang=eng&topic=15&catid=37", + "author": "", + "image": "itbriefing_logo_178x138.jpg", + "deck": "", + "publication": "ITBriefing.net", + "publish_date": "September 6, 2012" + }, + + { + "title": "Student Loans: Debt for Life", + "url": "http://www.businessweek.com/articles/2012-09-06/student-loans-debt-for-life#p3", + "author": "Peter Coy", + "image": "bloomberg_logo_178x138.jpeg", + "deck": "", + "publication": "Bloomberg Business Week", + "publish_date": "September 6, 2012" + }, + { + "title": "Straighterline wants to help professors expand reach, while students save", + "url": "http://www.baltimoresun.com/business/technology/blog/bs-bz-straighterline-college-professors-20120904,0,6114022.story", + "author": "Gus G. Sentementes", + "image": "baltsun_logo_178x138.jpg", + "deck": "", + "publication": "The Baltimore Sun", + "publish_date": "September 4, 2012" + }, + { + "title": "Want to be a reporter? Learn to code", + "url": "http://gigaom.com/cloud/want-to-be-a-reporter-learn-to-code/", + "author": "Barb Darrow", + "image": "gigaom_logo_178x138.jpeg", + "deck": "", + "publication": "GigaOM", + "publish_date": "September 4, 2012" + }, + { + "title": "MOOC Brigade: Will Massive, Open Online Courses Revolutionize Higher Education?", + "url": "http://nation.time.com/2012/09/04/mooc-brigade-will-massive-open-online-courses-revolutionize-higher-education/", + "author": "Kayla Webley", + "image": "time_logo_178x138.jpg", + "deck": "", + "publication": "Time", + "publish_date": "September 4, 2012" + }, + { + "title": "Ivy walls lower with free online classes from Coursera and edX ", + "url": "http://www.csmonitor.com/Innovation/Pioneers/2012/0903/Ivy-walls-lower-with-free-online-classes-from-Coursera-and-edX", + "author": "Chris Gaylord", + "image": "csmonitor_logo_178x138.jpg", + "deck": "", + "publication": "Christian Science Monitor", + "publish_date": "September 3, 2012" + }, + { + "title": "Summer recap. RLADs, new edX partner, Institute files amicus brief", + "url": "http://tech.mit.edu/V132/N34/summer.html", + "author": "", + "image": "thetech_logo_178x138.jpg", + "deck": "", + "publication": "The Tech", + "publish_date": "September 4, 2012" + }, + { + "title": "Into the Future With MOOC's", + "url": "http://chronicle.com/article/Into-the-Future-With-MOOCs/134080/", + "author": "Kevin Carey", + "image": "chroniclehighered_logo_178x138.jpeg", + "deck": "", + "publication": "The Chronicle of Higher Education", + "publish_date": "September 3, 2012" + }, + { + "title": "The Future Of Higher Education", + "url": "http://radioboston.wbur.org/2012/08/20/higher-education-online", + "author": "", + "image": "radioboston_logo_178x138.jpg", + "deck": "", + "publication": "NPR/Radio Boston", + "publish_date": "August 20, 2012" + }, + { + "title": "Berkeley Joins edX", + "url": "http://www.insidehighered.com/quicktakes/2012/07/24/berkeley-joins-edx", + "author": "Tamar Lewin", + "image": "insidehighered_logo_178x138.jpg", + "deck": "", + "publication": "Inside Higher Ed", + "publish_date": "July 24, 2012" + }, { "title": "Berkeley to Join the Free Online Learning Partnership EdX", "url": "http://www.nytimes.com/2012/07/24/education/berkeley-to-offer-free-online-classes-on-edx.html?_r=1", diff --git a/lms/templates/static_templates/press.html b/lms/templates/static_templates/press.html index 6294b346a9..277cb91bd2 100644 --- a/lms/templates/static_templates/press.html +++ b/lms/templates/static_templates/press.html @@ -37,4 +37,3 @@ % endfor

- From 596d926eacc455815c86f095f2943b311d47d728 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 11:06:01 -0400 Subject: [PATCH 28/66] Added fix so that sequnce nave doesn't go over full screen videos --- common/lib/xmodule/xmodule/css/sequence/display.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 25d2c26dda..0533465298 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -32,7 +32,7 @@ nav.sequence-nav { .sequence-list-wrapper { position: relative; - z-index: 9999; + z-index: 99; border: 1px solid #ccc; height: 44px; margin: 0 30px; From 2f8a7d0744df7330ea6d526f06cb8c438be74365 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 12:37:19 -0400 Subject: [PATCH 29/66] Removes unused widowfix script --- lms/templates/courseware/courseware.html | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index a433ddc9fc..67719eacc9 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -12,7 +12,6 @@ - ## codemirror From ec9521a9f543c6d03c3e30140b7340a2b90b9c41 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 13:01:10 -0400 Subject: [PATCH 30/66] removed list bullets from tutorials --- lms/static/sass/course/courseware/_courseware.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 863bcad139..0b79ec6a6b 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -116,6 +116,7 @@ div.course-wrapper { margin: 0; @include clearfix(); padding: 0; + list-style: none; li { width: flex-grid(3, 9); From e47134bd1097a1c703ab1065dd9d914ca8e802f5 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 12 Sep 2012 13:20:15 -0400 Subject: [PATCH 31/66] turn off psychometrics in dev.py--the necessary migration is not present --- lms/envs/dev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 5a7e019e55..a9f1454193 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -20,7 +20,7 @@ MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains- MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True -MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = True # real-time psychometrics (eg item response theory analysis in instructor dashboard) +MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) WIKI_ENABLED = True From b6b9b1591242a165c6d9365f81a012bcd4e9a705 Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 12 Sep 2012 14:58:23 -0300 Subject: [PATCH 32/66] Make clear where to specify display_name in xml --- doc/xml-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/xml-format.md b/doc/xml-format.md index e1439ee97d..29c60fea99 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -299,7 +299,7 @@ This is a sketch ("tue" is not a valid start date), that should help illustrate ## Specifying metadata in the xml file -Metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, you should be consistent (e.g. if `display_name`s stay in xml, they should all stay in xml). +Metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, you should be consistent (e.g. if `display_name`s stay in xml, they should all stay in xml. Note `display_name` should be specified in the problem xml definition itself, ie, Problem Text , in file ProblemFoo.xml). - note, some xml attributes are not metadata. e.g. in `