From f3067cd8c88ed0b732ecc1059f6ccacebe80581e Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Fri, 14 Dec 2012 16:17:03 -0500 Subject: [PATCH 001/541] restyling of dashboard with pearson exam notifications --- lms/static/sass/multicourse/_dashboard.scss | 220 ++++++++------------ lms/templates/dashboard.html | 162 +++++++------- 2 files changed, 176 insertions(+), 206 deletions(-) diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 458888b658..249e8a0513 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -267,13 +267,12 @@ } .my-course { - @include border-radius(3px); - @include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8)); + clear: both; @include clearfix; - height: 120px; margin-right: flex-gutter(); - margin-bottom: 10px; - overflow: hidden; + margin-bottom: 50px; + padding-bottom: 50px; + border-bottom: 1px solid $light-gray; position: relative; width: flex-grid(12); z-index: 20; @@ -283,13 +282,7 @@ margin-bottom: none; } - .cover { - background: rgb(225,225,225); - background-size: cover; - background-position: center center; - border: 1px solid rgb(120,120,120); - @include border-left-radius(3px); - @include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.6), 1px 0 0 0 rgba(255,255,255, 0.8)); + .cover { @include box-sizing(border-box); float: left; height: 100%; @@ -299,100 +292,51 @@ position: relative; @include transition(all, 0.15s, linear); width: 200px; + height: 120px; - .shade { - @include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%, - rgba(0,0,0, 0.3) 100%)); - bottom: 0px; - content: ""; - display: block; - left: 0px; - position: absolute; - z-index: 50; - top: 0px; - @include transition(all, 0.15s, linear); - right: 0px; - } - - .arrow { - position: absolute; - z-index: 100; + img { width: 100%; - font-size: 70px; - line-height: 110px; - text-align: center; - text-decoration: none; - color: rgba(0, 0, 0, .7); - opacity: 0; - @include transition(all, 0.15s, linear); - } - - &:hover { - .shade { - background: rgba(255,255,255, 0.3); - @include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%, - rgba(0,0,0, 0.3) 100%)); - } } } .info { - background: rgb(250,250,250); - @include background-image(linear-gradient(-90deg, rgb(253,253,253), rgb(240,240,240))); - @include box-sizing(border-box); - border: 1px solid rgb(190,190,190); - border-left: none; - @include border-right-radius(3px); - left: 201px; - height: 100%; - max-height: 100%; - padding: 0px 10px; - position: absolute; - right: 0px; - top: 0px; - z-index: 2; + @include clearfix; + padding: 0 10px 0 230px; > hgroup { - @include clearfix; - border-bottom: 1px solid rgb(210,210,210); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); - padding: 12px 0px; + padding: 0; width: 100%; .university { - background: rgba(255,255,255, 1); - border: 1px solid rgb(180,180,180); - @include border-radius(3px); - @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.2), 0 1px 0 0 rgba(255,255,255, 0.6)); color: $lighter-base-font-color; - display: block; - font-style: italic; font-family: $sans-serif; font-size: 16px; - font-weight: 800; - @include inline-block; - margin-right: 10px; - margin-bottom: 0; - padding: 5px 10px; - float: left; + font-weight: 400; + margin: 0 0 6px; + text-transform: none; + letter-spacing: 0; } - h3 { + .date-block { + position: absolute; + top: 0; + right: 0; + font-family: $sans-serif; + font-size: 13px; + font-style: italic; + color: $lighter-base-font-color; + } + + h3 a { display: block; - margin-bottom: 0px; - overflow: hidden; - padding-top: 2px; - text-overflow: ellipsis; - white-space: nowrap; + margin-bottom: 10px; + font-family: $sans-serif; + font-size: 34px; + line-height: 42px; + font-weight: 300; - a { - color: $base-font-color; - font-weight: 700; - text-shadow: 0 1px rgba(255,255,255, 0.6); - - &:hover { - text-decoration: underline; - } + &:hover { + text-decoration: none; } } } @@ -430,71 +374,52 @@ } .enter-course { - @include button(shiny, $blue); + @include button(simple, $blue); @include box-sizing(border-box); @include border-radius(3px); display: block; float: left; - font: normal 1rem/1.6rem $sans-serif; - letter-spacing: 1px; - padding: 6px 0px; - text-transform: uppercase; + font: normal 15px/1.6rem $sans-serif; + letter-spacing: 0; + padding: 6px 32px 7px; text-align: center; margin-top: 16px; - width: flex-grid(4); - } - } - > a:hover { - .cover { - .shade { - background: rgba(255,255,255, 0.1); - @include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%, - rgba(0,0,0, 0.3) 100%)); + &.archived { + @include button(simple, #eee); + font: normal 15px/1.6rem $sans-serif; + padding: 6px 32px 7px; + + &:hover { + text-decoration: none; + } } - .arrow { - opacity: 1; - } - } - - .info { - background: darken(rgb(250,250,250), 5%); - @include background-image(linear-gradient(-90deg, darken(rgb(253,253,253), 3%), darken(rgb(240,240,240), 5%))); - border-color: darken(rgb(190,190,190), 10%); - - .course-status { - background: darken($yellow, 3%); - border-color: darken(rgb(200,200,200), 3%); - @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); - } - - .course-status-completed { - background: #888; - color: #fff; + &:hover { + text-decoration: none; } } } } .message-status { + @include clearfix; @include border-radius(3px); - @include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8)); display: none; - position: relative; - top: -15px; z-index: 10; - margin: 0 0 20px 0; + margin: 20px 0 10px; padding: 15px 20px; - font-family: "Open Sans", Verdana, Geneva, sans-serif; + font-family: $sans-serif; background: #fffcf0; border: 1px solid #ccc; .message-copy { + font-family: $sans-serif; + font-size: 13px; margin: 0; .grade-value { - font-size: 1.4rem; + font-size: 1.2rem; font-weight: bold; } } @@ -502,19 +427,18 @@ .actions { @include clearfix; list-style: none; - margin: 15px 0 0 0; + margin: 0; padding: 0; .action { float: left; - margin:0 15px 10px 0; + margin: 0 15px 0 0; .btn, .cta { display: inline-block; } .btn { - @include button(shiny, $blue); @include box-sizing(border-box); @include border-radius(3px); float: left; @@ -524,7 +448,6 @@ text-align: center; &.disabled { - @include button(shiny, #eee); cursor: default !important; &:hover { @@ -539,7 +462,6 @@ } .cta { - @include button(shiny, #666); float: left; font: normal 0.8rem/1.2rem $sans-serif; letter-spacing: 1px; @@ -549,6 +471,35 @@ } } + .exam-registration-number { + font-family: $sans-serif; + font-size: 18px; + } + + &.exam-register { + .message-copy { + margin-top: 5px; + } + } + + &.exam-schedule { + .exam-button { + margin-top: 5px; + } + } + + .exam-button { + @include button(simple, $pink); + float: right; + padding: 9px 18px 10px; + font-size: 13px; + font-weight: 400; + + &:hover { + text-decoration: none; + } + } + &.is-shown { display: block; } @@ -577,17 +528,16 @@ a.unenroll { float: right; + display: block; font-style: italic; color: #a0a0a0; text-decoration: underline; font-size: .8em; - @include inline-block; - margin-bottom: 40px; + margin-top: 32px; &:hover { color: #333; } } - } } diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index d9b57ac044..0c920afbed 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -198,87 +198,107 @@ course_target = reverse('about_course', args=[course.id]) %> - -
-
-
-
-
-
-

${get_course_about_section(course, 'university')}

-

${course.number} ${course.title}

-
-
-

+ + + + + +

+
+

% if course.has_ended(): - Course Completed - ${course.end_date_text} + Course Completed - ${course.end_date_text} % elif course.has_started(): - Course Started - ${course.start_date_text} + Course Started - ${course.start_date_text} % else: # hasn't started yet - Course Starts - ${course.start_date_text} + Course Starts - ${course.start_date_text} % endif

-
- % if course.id in show_courseware_links_for: -

View Courseware

- % endif -
- - +

${get_course_about_section(course, 'university')}

+

${course.number} ${course.title}

+ - <% - cert_status = cert_statuses.get(course.id) - %> - % if course.has_ended() and cert_status: - <% - if cert_status['status'] == 'generating': - status_css_class = 'course-status-certrendering' - elif cert_status['status'] == 'ready': - status_css_class = 'course-status-certavailable' - elif cert_status['status'] == 'notpassing': - status_css_class = 'course-status-certnotavailable' - else: - status_css_class = 'course-status-processing' - %> -
- - % if cert_status['status'] == 'processing': -

Final course details are being wrapped up at - this time. Your final standing will be available shortly.

- % elif cert_status['status'] in ('generating', 'ready', 'notpassing'): -

Your final grade: - ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. - % if cert_status['status'] == 'notpassing': - Grade required for a certificate: - ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}. - % endif -

- % endif - - % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: - - % endif +
+ Register for Pearson exam +

Registration for the Pearson exam is now open.

- % endif +
+

Your registration for the Pearson exam is pending. Within a few days, you should receive a confirmation number, which can be used to schedule your exam.

+
- Unregister +
+ Schedule Pearson exam +

Registration number: edx00015879548

+

Write this down! You’ll need it to schedule your exam.

+
+ + + + <% + cert_status = cert_statuses.get(course.id) + %> + % if course.has_ended() and cert_status: + <% + if cert_status['status'] == 'generating': + status_css_class = 'course-status-certrendering' + elif cert_status['status'] == 'ready': + status_css_class = 'course-status-certavailable' + elif cert_status['status'] == 'notpassing': + status_css_class = 'course-status-certnotavailable' + else: + status_css_class = 'course-status-processing' + %> +
+ + % if cert_status['status'] == 'processing': +

Final course details are being wrapped up at + this time. Your final standing will be available shortly.

+ % elif cert_status['status'] in ('generating', 'ready', 'notpassing'): +

Your final grade: + ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. + % if cert_status['status'] == 'notpassing': + Grade required for a certificate: + ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}. + % endif +

+ % endif + + % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: + + % endif +
+ + % endif + + % if course.id in show_courseware_links_for: + % if course.has_ended(): + View Archived Course + % else: + View Course + % endif + % endif + Unregister +
+ + + % endfor % else: From b2117c1194b7a19c023a68ee1242876fd10639d6 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 17 Dec 2012 17:59:44 -0500 Subject: [PATCH 002/541] first dialogs --- lms/templates/dashboard.html | 3 +- lms/templates/test_center_register_modal.html | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 lms/templates/test_center_register_modal.html diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 0c920afbed..85106ee1c2 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -219,8 +219,9 @@

${course.number} ${course.title}

+
- Register for Pearson exam + Register for Pearson exam

Registration for the Pearson exam is now open.

diff --git a/lms/templates/test_center_register_modal.html b/lms/templates/test_center_register_modal.html new file mode 100644 index 0000000000..84db6dd09b --- /dev/null +++ b/lms/templates/test_center_register_modal.html @@ -0,0 +1,105 @@ +<%namespace name='static' file='static_content.html'/> +<%! from django.core.urlresolvers import reverse %> +<%! from django_countries.countries import COUNTRIES %> +<%! from student.models import UserProfile %> +<%! from datetime import date %> +<%! import calendar %> + + + + From c7d379beb672b57293e62b11fe7031c2049d09d9 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 18 Dec 2012 03:20:02 -0500 Subject: [PATCH 003/541] add first pass at wiring test registration dialog --- common/djangoapps/student/views.py | 90 ++++++++++++++++++- lms/templates/dashboard.html | 3 +- lms/templates/test_center_register_modal.html | 58 ++++++------ lms/urls.py | 2 + 4 files changed, 123 insertions(+), 30 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 44877ef597..ad2f810b1f 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -26,7 +26,7 @@ from bs4 import BeautifulSoup from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt -from student.models import (Registration, UserProfile, +from student.models import (Registration, UserProfile, TestCenterUser, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user) @@ -205,6 +205,14 @@ def dashboard(request): user = request.user enrollments = CourseEnrollment.objects.filter(user=user) + # we want to populate the registration page with the relevant information, + # if it already exists. Create an empty object otherwise. + try: + testcenteruser = TestCenterUser.objects.get(user=user) + except TestCenterUser.DoesNotExist: + testcenteruser = TestCenterUser() + testcenteruser.user = user + # Build our courses list for the user, but ignore any courses that no longer # exist (because the course IDs have changed). Still, we don't delete those # enrollments, because it could have been a data push snafu. @@ -244,6 +252,7 @@ def dashboard(request): 'show_courseware_links_for' : show_courseware_links_for, 'cert_statuses': cert_statuses, 'news': top_news, + 'testcenteruser': testcenteruser, } return render_to_response('dashboard.html', context) @@ -586,6 +595,83 @@ def create_account(request, post_override=None): js = {'success': True} return HttpResponse(json.dumps(js), mimetype="application/json") +@ensure_csrf_cookie +def create_test_registration(request, post_override=None): + ''' + JSON call to create test registration. + Used by form in test_center_register_modal.html, which is included + into dashboard.html + ''' + js = {'success': False} + + post_vars = post_override if post_override else request.POST + + # Confirm we have a properly formed request + for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: + if a not in post_vars: + js['value'] = "Error (401 {field}). E-mail us.".format(field=a) + js['field'] = a + return HttpResponse(json.dumps(js)) + + # Confirm appropriate fields are there. + for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: + if len(post_vars[a]) < 2: + error_str = {'first_name': 'First name must be minimum of two characters long.', + 'last_name': 'Last name must be minimum of two characters long.', + 'address_1': 'Last name must be minimum of two characters long.', + 'city': 'Last name must be minimum of two characters long.', + 'country': 'Last name must be minimum of two characters long.', + } + js['value'] = error_str[a] + js['field'] = a + return HttpResponse(json.dumps(js)) + + # Once the test_center_user information has been validated, create the entries: + ret = _do_create_or_update_test_center_user(post_vars) + if isinstance(ret,HttpResponse): # if there was an error then return that + return ret + + + (user, profile, testcenter_user, testcenter_registration) = ret + + + # only do the following if there is accommodation text to send, + # and a destination to which to send it: + if 'accommodation' in post_vars and settings.MITX_FEATURES.get('ACCOMMODATION_EMAIL'): + d = {'accommodation': post_vars['accommodation'] + } + + # composes accommodation email + subject = render_to_string('emails/accommodation_email_subject.txt', d) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + message = render_to_string('emails/accommodation_email.txt', d) + + # skip if destination email address is not specified + try: + dest_addr = settings.MITX_FEATURES['ACCOMMODATION_EMAIL'] + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False) + except: + log.exception(sys.exc_info()) + js['value'] = 'Could not send accommodation e-mail.' + return HttpResponse(json.dumps(js)) + + + if DoExternalAuth: + eamap.user = login_user + eamap.dtsignup = datetime.datetime.now() + eamap.save() + log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'],eamap)) + + if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): + log.debug('bypassing activation email') + login_user.is_active = True + login_user.save() + + # statsd.increment("common.student.account_created") + + js = {'success': True} + return HttpResponse(json.dumps(js), mimetype="application/json") def get_random_post_override(): """ @@ -641,7 +727,7 @@ def password_reset(request): # By default, Django doesn't allow Users with is_active = False to reset their passwords, # but this bites people who signed up a long time ago, never activated, and forgot their - # password. So for their sake, we'll auto-activate a user for whome password_reset is called. + # password. So for their sake, we'll auto-activate a user for whom password_reset is called. try: user = User.objects.get(email=request.POST['email']) user.is_active = True diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 85106ee1c2..f8e4b2ab57 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -7,6 +7,7 @@ <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> +<%include file="test_center_register_modal.html" /> <%block name="title">Dashboard @@ -221,7 +222,7 @@
- Register for Pearson exam + Register for Pearson exam

Registration for the Pearson exam is now open.

diff --git a/lms/templates/test_center_register_modal.html b/lms/templates/test_center_register_modal.html index 84db6dd09b..5638d4750b 100644 --- a/lms/templates/test_center_register_modal.html +++ b/lms/templates/test_center_register_modal.html @@ -17,6 +17,10 @@
+ + + +
- - - - - - + + + + + + - + - +
- - - - - - + + + + + + - + - + - + - +
- + - - - + + + - - - - - + + + + +
diff --git a/lms/urls.py b/lms/urls.py index 0a76907380..97f4613b8f 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -43,6 +43,8 @@ urlpatterns = ('', url(r'^create_account$', 'student.views.create_account'), url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name="activate"), + url(r'^create_test_registration$', 'student.views.create_test_registration'), + url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'), ## Obsolete Django views for password resets ## TODO: Replace with Mako-ized views From bc40a7f12719b8a2d6d7164bb8087d2a1ec584be Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 18 Dec 2012 16:16:55 -0500 Subject: [PATCH 004/541] make test-reg dialog non-modal, and pass course_id in URL. Add to course info. --- common/djangoapps/student/views.py | 88 +++++++++- common/lib/xmodule/xmodule/course_module.py | 12 ++ lms/templates/dashboard.html | 36 ++-- lms/templates/test_center_register.html | 179 ++++++++++++++++++++ lms/urls.py | 1 + 5 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 lms/templates/test_center_register.html diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index ad2f810b1f..52fcbb0152 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -304,7 +304,7 @@ def change_enrollment(request): try: course = course_from_id(course_id) except ItemNotFoundError: - log.warning("User {0} tried to enroll in non-existant course {1}" + log.warning("User {0} tried to enroll in non-existent course {1}" .format(user.username, enrollment.course_id)) return {'success': False, 'error': 'The course requested does not exist.'} @@ -595,6 +595,92 @@ def create_account(request, post_override=None): js = {'success': True} return HttpResponse(json.dumps(js), mimetype="application/json") +@login_required +@ensure_csrf_cookie +def begin_test_registration(request, course_id): + user = request.user + + # we want to populate the registration page with the relevant information, + # if it already exists. Create an empty object otherwise. + try: + testcenteruser = TestCenterUser.objects.get(user=user) + except TestCenterUser.DoesNotExist: + testcenteruser = TestCenterUser() + testcenteruser.user = user + + try: + course = (course_from_id(course_id)) + except ItemNotFoundError: + log.error("User {0} enrolled in non-existent course {1}" + .format(user.username, course_id)) + + # placeholder for possible messages... + message = "" + if not user.is_active: + message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) + + context = {'course': course, + 'user': user, + 'message': message, + 'testcenteruser': testcenteruser, + } + + return render_to_response('test_center_register.html', context) + + +def _do_create_or_update_test_center_user(post_vars): + """ + Given cleaned post variables, create the TestCenterUser and UserProfile objects, as well as the + registration for this user. + + Returns a tuple (User, UserProfile, TestCenterUser). + + Note: this function is also used for creating test users. + """ + user = User(username=post_vars['username'], + email=post_vars['email'], + is_active=False) + user.set_password(post_vars['password']) + registration = Registration() + # TODO: Rearrange so that if part of the process fails, the whole process fails. + # Right now, we can have e.g. no registration e-mail sent out and a zombie account + try: + user.save() + except IntegrityError: + js = {'success': False} + # Figure out the cause of the integrity error + if len(User.objects.filter(username=post_vars['username'])) > 0: + js['value'] = "An account with this username already exists." + js['field'] = 'username' + return HttpResponse(json.dumps(js)) + + if len(User.objects.filter(email=post_vars['email'])) > 0: + js['value'] = "An account with this e-mail already exists." + js['field'] = 'email' + return HttpResponse(json.dumps(js)) + + raise + + registration.register(user) + + profile = UserProfile(user=user) + profile.name = post_vars['name'] + profile.level_of_education = post_vars.get('level_of_education') + profile.gender = post_vars.get('gender') + profile.mailing_address = post_vars.get('mailing_address') + profile.goals = post_vars.get('goals') + + try: + profile.year_of_birth = int(post_vars['year_of_birth']) + except (ValueError, KeyError): + profile.year_of_birth = None # If they give us garbage, just ignore it instead + # of asking them to put an integer. + try: + profile.save() + except Exception: + log.exception("UserProfile creation failed for user {0}.".format(user.id)) + return (user, profile, registration) + @ensure_csrf_cookie def create_test_registration(request, post_override=None): ''' diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 3506c72bd7..a4cf87b333 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -316,6 +316,18 @@ class CourseDescriptor(SequenceDescriptor): """ return self.metadata.get('end_of_course_survey_url') + @property + def testcenter_info(self): + """ + Pull from policy. + + TODO: decide if we expect this entry to be a single test, or if multiple tests are possible + per course. + + Returns None if no testcenter info specified. + """ + return self.metadata.get('testcenter_info') + @property def title(self): return self.display_name diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index f8e4b2ab57..f771f3e098 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -7,7 +7,6 @@ <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> -<%include file="test_center_register_modal.html" /> <%block name="title">Dashboard @@ -221,22 +220,31 @@ -
- Register for Pearson exam -

Registration for the Pearson exam is now open.

-
+ <% + testcenter_info = course.testcenter_info + %> + % if testcenter_info is not None: + + <% + testcenter_register_target = reverse('begin_test_registration', args=[course.id]) + %> +
+ Register for Pearson exam +

Registration for the Pearson exam is now open.

+
-
-

Your registration for the Pearson exam is pending. Within a few days, you should receive a confirmation number, which can be used to schedule your exam.

-
+
+

Your registration for the Pearson exam is pending. Within a few days, you should receive a confirmation number, which can be used to schedule your exam.

+
-
- Schedule Pearson exam -

Registration number: edx00015879548

-

Write this down! You’ll need it to schedule your exam.

-
+
+ Schedule Pearson exam +

Registration number: edx00015879548

+

Write this down! You’ll need it to schedule your exam.

+
- + % endif <% cert_status = cert_statuses.get(course.id) diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html new file mode 100644 index 0000000000..c71c40610d --- /dev/null +++ b/lms/templates/test_center_register.html @@ -0,0 +1,179 @@ +<%! + from django.core.urlresolvers import reverse + from courseware.courses import course_image_url, get_course_about_section + from courseware.access import has_access + from certificates.models import CertificateStatuses +%> +<%inherit file="main.html" /> + +<%namespace name='static' file='static_content.html'/> + +<%block name="title">Sign Up for Pearson VUE Test Center Proctoring + +<%block name="js_extra"> + + + +
+ + + %if message: +
+ ${message} +
+ %endif + +
+
+
+

+
+
+ + + +
+
+

+ % if course.has_ended(): + Course Completed - ${course.end_date_text} + % elif course.has_started(): + Course Started - ${course.start_date_text} + % else: # hasn't started yet + Course Starts - ${course.start_date_text} + % endif +

+

${get_course_about_section(course, 'university')}

+

${course.number} ${course.title}

+
+ + + <% + testcenter_info = course.testcenter_info + %> + % if testcenter_info is not None: + <% + exam_info = testcenter_info.get('Final_Exam') + %> +

Exam Series Code: ${exam_info.get('Exam_Series_Code')}

+

First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

+

Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

+ % endif + +
+ + +
+
+ + +
+ + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ +
+ + +
+ + +
+ + +
+ + + + + +
+ + + + +
+ +
+ + + + + + +
+ + + + +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 97f4613b8f..e81e25e86a 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -43,6 +43,7 @@ urlpatterns = ('', url(r'^create_account$', 'student.views.create_account'), url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name="activate"), + url(r'^begin_test_registration/(?P[^/]+/[^/]+/[^/]+)$', 'student.views.begin_test_registration', name="begin_test_registration"), url(r'^create_test_registration$', 'student.views.create_test_registration'), url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'), From f912ffe8a2968413b5ffdb61414de9f7cb9d0b70 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 18 Dec 2012 18:31:41 -0500 Subject: [PATCH 005/541] add persistence of testcenter_user registration --- common/djangoapps/student/models.py | 15 ++++ common/djangoapps/student/views.py | 113 +++++++++++++++------------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 2f5bc3ac04..89e2548bbb 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -189,10 +189,25 @@ class TestCenterUser(models.Model): # Company company_name = models.CharField(max_length=50, blank=True) + @staticmethod + def user_provided_fields(): + return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', + 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country', + 'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name'] + @property def email(self): return self.user.email +class TestCenterRegistration(models.Model): + testcenter_user = models.ForeignKey(TestCenterUser, unique=True, default=None) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + updated_at = models.DateTimeField(auto_now=True, db_index=True) + accommodation_request = models.CharField(max_length=1024) + # TODO: this should be an enumeration: + accommodation_code = models.CharField(max_length=64) + + def unique_id_for_user(user): """ Return a unique id for a user, suitable for inserting into diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 52fcbb0152..d972cb326c 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -26,7 +26,7 @@ from bs4 import BeautifulSoup from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt -from student.models import (Registration, UserProfile, TestCenterUser, +from student.models import (Registration, UserProfile, TestCenterUser, TestCenterRegistration, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user) @@ -614,7 +614,7 @@ def begin_test_registration(request, course_id): log.error("User {0} enrolled in non-existent course {1}" .format(user.username, course_id)) - # placeholder for possible messages... + # TODO: placeholder for possible messages... message = "" if not user.is_active: message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) @@ -634,52 +634,68 @@ def _do_create_or_update_test_center_user(post_vars): registration for this user. Returns a tuple (User, UserProfile, TestCenterUser). - - Note: this function is also used for creating test users. + """ - user = User(username=post_vars['username'], - email=post_vars['email'], - is_active=False) - user.set_password(post_vars['password']) - registration = Registration() + + # first determine if we need to create a new TestCenterUser, or if we are making any update + # to an existing TestCenterUser. + username=post_vars['username'] + user = User.objects.get(username=username) + + needs_saving = False + try: + testcenter_user = TestCenterUser.objects.get(user=user) + # found a TestCenterUser, so check to see if it has changed + needs_updating = any([testcenter_user.__getattribute__(fieldname) != post_vars[fieldname] + for fieldname in TestCenterUser.user_provided_fields()]) + + if needs_updating: + # TODO: what do we set a timestamp to, in order to get now()? + testcenter_user.user_updated_at = datetime.datetime.now() + # Now do the update: + for fieldname in TestCenterUser.user_provided_fields(): + testcenter_user.__setattr__(fieldname, post_vars[fieldname]) + needs_saving = True + + except TestCenterUser.DoesNotExist: + # did not find the TestCenterUser, so create a new one + testcenter_user = TestCenterUser(user=user) + # testcenter_user.user = user + for fieldname in TestCenterUser.user_provided_fields(): + testcenter_user.__setattr__(fieldname, post_vars[fieldname]) + # testcenter_user.candidate_id remains unset + testcenter_user.client_candidate_id = 'edx' + '123456' # some unique value + testcenter_user.user_updated_at = datetime.datetime.now() + needs_saving = True + + # additional validation occurs at save time, so handle exceptions # TODO: Rearrange so that if part of the process fails, the whole process fails. # Right now, we can have e.g. no registration e-mail sent out and a zombie account - try: - user.save() - except IntegrityError: - js = {'success': False} - # Figure out the cause of the integrity error - if len(User.objects.filter(username=post_vars['username'])) > 0: - js['value'] = "An account with this username already exists." - js['field'] = 'username' - return HttpResponse(json.dumps(js)) + if needs_saving: + try: + testcenter_user.save() + except IntegrityError: + js = {'success': False} + # TODO: Figure out the cause of the integrity error + if len(User.objects.filter(username=post_vars['username'])) > 0: + js['value'] = "An account with this username already exists." + js['field'] = 'username' + return HttpResponse(json.dumps(js)) + + if len(User.objects.filter(email=post_vars['email'])) > 0: + js['value'] = "An account with this e-mail already exists." + js['field'] = 'email' + return HttpResponse(json.dumps(js)) - if len(User.objects.filter(email=post_vars['email'])) > 0: - js['value'] = "An account with this e-mail already exists." - js['field'] = 'email' - return HttpResponse(json.dumps(js)) + raise + + + registration = TestCenterRegistration(testcenter_user = testcenter_user) + # registration.register(user) - raise + registration.accommodation_request = post_vars['accommodations'] - registration.register(user) - - profile = UserProfile(user=user) - profile.name = post_vars['name'] - profile.level_of_education = post_vars.get('level_of_education') - profile.gender = post_vars.get('gender') - profile.mailing_address = post_vars.get('mailing_address') - profile.goals = post_vars.get('goals') - - try: - profile.year_of_birth = int(post_vars['year_of_birth']) - except (ValueError, KeyError): - profile.year_of_birth = None # If they give us garbage, just ignore it instead - # of asking them to put an integer. - try: - profile.save() - except Exception: - log.exception("UserProfile creation failed for user {0}.".format(user.id)) - return (user, profile, registration) + return (user, testcenter_user, registration) @ensure_csrf_cookie def create_test_registration(request, post_override=None): @@ -718,7 +734,7 @@ def create_test_registration(request, post_override=None): return ret - (user, profile, testcenter_user, testcenter_registration) = ret + (user, testcenter_user, testcenter_registration) = ret # only do the following if there is accommodation text to send, @@ -743,17 +759,6 @@ def create_test_registration(request, post_override=None): return HttpResponse(json.dumps(js)) - if DoExternalAuth: - eamap.user = login_user - eamap.dtsignup = datetime.datetime.now() - eamap.save() - log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'],eamap)) - - if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): - log.debug('bypassing activation email') - login_user.is_active = True - login_user.save() - # statsd.increment("common.student.account_created") js = {'success': True} From f472ac60f511081ef75fb40af1dc610fb173ca23 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 19 Dec 2012 00:21:49 -0500 Subject: [PATCH 006/541] minor tweaks to test center registration --- common/djangoapps/student/models.py | 30 ++++++++++++++++++++++++++--- common/djangoapps/student/views.py | 15 ++++++++------- lms/templates/dashboard.html | 3 ++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 89e2548bbb..b7b2b4e21d 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -200,13 +200,37 @@ class TestCenterUser(models.Model): return self.user.email class TestCenterRegistration(models.Model): + """ + This is our representation of a user's registration for in-person testing, + and specifically for Pearson at this point. A few things to note: + + * Pearson only supports Latin-1, so we have to make sure that the data we + capture here will work with that encoding. This is less of an issue + than for the TestCenterUser. + * Registrations are only created here when a user registers to take an exam in person. + + The field names and lengths are modeled on the conventions and constraints + of Pearson's data import system. + """ + # TODO: Check the spec to find out lengths specified by Pearson + testcenter_user = models.ForeignKey(TestCenterUser, unique=True, default=None) created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) - accommodation_request = models.CharField(max_length=1024) - # TODO: this should be an enumeration: - accommodation_code = models.CharField(max_length=64) + course_id = models.CharField(max_length=128, db_index=True) + # store the original text of the accommodation request. + accommodation_request = models.CharField(max_length=1024, blank=True) + # TODO: this should be an enumeration: + accommodation_code = models.CharField(max_length=64, blank=True) + + @property + def candidate_id(self): + return self.testcenter_user.candidate_id + + @property + def client_candidate_id(self): + return self.testcenter_user.client_candidate_id def unique_id_for_user(user): """ diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index d972cb326c..cfb6b3a39b 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -207,11 +207,11 @@ def dashboard(request): # we want to populate the registration page with the relevant information, # if it already exists. Create an empty object otherwise. - try: - testcenteruser = TestCenterUser.objects.get(user=user) - except TestCenterUser.DoesNotExist: - testcenteruser = TestCenterUser() - testcenteruser.user = user +# try: +# testcenteruser = TestCenterUser.objects.get(user=user) +# except TestCenterUser.DoesNotExist: +# testcenteruser = TestCenterUser() +# testcenteruser.user = user # Build our courses list for the user, but ignore any courses that no longer # exist (because the course IDs have changed). Still, we don't delete those @@ -252,7 +252,8 @@ def dashboard(request): 'show_courseware_links_for' : show_courseware_links_for, 'cert_statuses': cert_statuses, 'news': top_news, - 'testcenteruser': testcenteruser, +# No longer needed here...move to begin_registration +# 'testcenteruser': testcenteruser, } return render_to_response('dashboard.html', context) @@ -692,7 +693,7 @@ def _do_create_or_update_test_center_user(post_vars): registration = TestCenterRegistration(testcenter_user = testcenter_user) # registration.register(user) - + registration.course_id = post_vars['course_id'] registration.accommodation_request = post_vars['accommodations'] return (user, testcenter_user, registration) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index f771f3e098..c7a8df077a 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -239,7 +239,8 @@
- Schedule Pearson exam + + Schedule Pearson exam

Registration number: edx00015879548

Write this down! You’ll need it to schedule your exam.

From ee99080687a8f9b7312bab14e02d6a4ea4de7a7a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 19 Dec 2012 16:53:13 -0500 Subject: [PATCH 007/541] add additional fields to testcenter user and update test center registration. --- ...022_add_more_fields_to_test_center_user.py | 198 ++++++++++++++++++ common/djangoapps/student/models.py | 50 ++++- common/djangoapps/student/views.py | 75 ++++--- common/lib/xmodule/xmodule/course_module.py | 15 +- lms/templates/dashboard.html | 20 +- lms/templates/test_center_register.html | 7 +- 6 files changed, 314 insertions(+), 51 deletions(-) create mode 100644 common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py diff --git a/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py new file mode 100644 index 0000000000..4a6481f80c --- /dev/null +++ b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'TestCenterUser.upload_status' + db.add_column('student_testcenteruser', 'upload_status', + self.gf('django.db.models.fields.CharField')(default='', max_length=20, blank=True), + keep_default=False) + + # Adding field 'TestCenterUser.confirmed_at' + db.add_column('student_testcenteruser', 'confirmed_at', + self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True), + keep_default=False) + + # Adding field 'TestCenterUser.upload_error_message' + db.add_column('student_testcenteruser', 'upload_error_message', + self.gf('django.db.models.fields.CharField')(default='', max_length=512, blank=True), + keep_default=False) + + # Adding model 'TestCenterRegistration' + db.create_table('student_testcenterregistration', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('testcenter_user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['student.TestCenterUser'], unique=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), + ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + ('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('client_authorization_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=20, db_index=True)), + ('exam_series_code', self.gf('django.db.models.fields.CharField')(max_length=15, db_index=True)), + ('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)), + ('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)), + ('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), + ('accommodation_request', self.gf('django.db.models.fields.CharField')(max_length=1024, blank=True)), + ('upload_status', self.gf('django.db.models.fields.CharField')(max_length=20, blank=True)), + ('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)), + )) + db.send_create_signal('student', ['TestCenterRegistration']) + + + def backwards(self, orm): + # Deleting model 'TestCenterRegistration' + db.delete_table('student_testcenterregistration') + + # Deleting field 'TestCenterUser.upload_status' + db.delete_column('student_testcenteruser', 'upload_status') + + # Deleting field 'TestCenterUser.confirmed_at' + db.delete_column('student_testcenteruser', 'confirmed_at') + + # Deleting field 'TestCenterUser.upload_error_message' + db.delete_column('student_testcenteruser', 'upload_error_message') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenterregistration': { + 'Meta': {'object_name': 'TestCenterRegistration'}, + 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'accommodation_request': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), + 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']", 'unique': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'upload_status': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'upload_status': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index b7b2b4e21d..6b73c2319f 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -141,6 +141,9 @@ class TestCenterUser(models.Model): The field names and lengths are modeled on the conventions and constraints of Pearson's data import system, including oddities such as suffix having a limit of 255 while last_name only gets 50. + + Also storing here the confirmation information received from Pearson (if any) + as to the success or failure of the upload. (VCDC file) """ # Our own record keeping... user = models.ForeignKey(User, unique=True, default=None) @@ -155,7 +158,7 @@ class TestCenterUser(models.Model): # we first create the User entry, and is assigned by Pearson later. candidate_id = models.IntegerField(null=True, db_index=True) - # Unique ID we assign our user for a the Test Center. + # Unique ID we assign our user for the Test Center. client_candidate_id = models.CharField(max_length=50, db_index=True) # Name @@ -189,6 +192,11 @@ class TestCenterUser(models.Model): # Company company_name = models.CharField(max_length=50, blank=True) + # Confirmation + upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' + confirmed_at = models.DateTimeField(null=True, db_index=True) + upload_error_message = models.CharField(max_length=512, blank=True) + @staticmethod def user_provided_fields(): return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', @@ -212,17 +220,38 @@ class TestCenterRegistration(models.Model): The field names and lengths are modeled on the conventions and constraints of Pearson's data import system. """ - # TODO: Check the spec to find out lengths specified by Pearson - + # to find an exam registration, we key off of the user and course_id. + # If multiple exams per course are possible, we would also need to add the + # exam_series_code. testcenter_user = models.ForeignKey(TestCenterUser, unique=True, default=None) + course_id = models.CharField(max_length=128, db_index=True) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) - course_id = models.CharField(max_length=128, db_index=True) + # user_updated_at happens only when the user makes a change to their data, + # and is something Pearson needs to know to manage updates. Unlike + # updated_at, this will not get incremented when we do a batch data import. + # The appointment dates, the exam count, and the accommodation codes can be updated, + # but hopefully this won't happen often. + user_updated_at = models.DateTimeField(db_index=True) + # "client_authorization_id" is the client's unique identifier for the authorization. + # This must be present for an update or delete to be sent to Pearson. + client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True) + + # information about the test, from the course policy: + exam_series_code = models.CharField(max_length=15, db_index=True) + eligibility_appointment_date_first = models.DateField(db_index=True) + eligibility_appointment_date_last = models.DateField(db_index=True) + # TODO: this should be an enumeration: + accommodation_code = models.CharField(max_length=64, blank=True) # store the original text of the accommodation request. accommodation_request = models.CharField(max_length=1024, blank=True) - # TODO: this should be an enumeration: - accommodation_code = models.CharField(max_length=64, blank=True) + + # Confirmation + upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' + confirmed_at = models.DateTimeField(db_index=True) + upload_error_message = models.CharField(max_length=512, blank=True) @property def candidate_id(self): @@ -232,6 +261,15 @@ class TestCenterRegistration(models.Model): def client_candidate_id(self): return self.testcenter_user.client_candidate_id + + +def get_testcenter_registrations_for_user_and_course(user, course_id): + try: + tcu = TestCenterUser.objects.get(user=user) + except User.DoesNotExist: + return [] + return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id) + def unique_id_for_user(user): """ Return a unique id for a user, suitable for inserting into diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index cfb6b3a39b..4d5728792e 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1,12 +1,12 @@ import datetime import feedparser -import itertools +#import itertools import json import logging import random import string import sys -import time +#import time import urllib import uuid @@ -19,7 +19,8 @@ from django.core.context_processors import csrf from django.core.mail import send_mail from django.core.validators import validate_email, validate_slug, ValidationError from django.db import IntegrityError -from django.http import HttpResponse, HttpResponseForbidden, Http404 +from django.http import HttpResponse, HttpResponseForbidden, Http404,\ + HttpResponseRedirect from django.shortcuts import redirect from mitxmako.shortcuts import render_to_response, render_to_string from bs4 import BeautifulSoup @@ -37,13 +38,14 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError -from datetime import date +#from datetime import date from collections import namedtuple from courseware.courses import get_courses_by_university from courseware.access import has_access from statsd import statsd +from django.contrib.localflavor.ie.ie_counties import IE_COUNTY_CHOICES log = logging.getLogger("mitx.student") Article = namedtuple('Article', 'title url author image deck publication publish_date') @@ -640,9 +642,11 @@ def _do_create_or_update_test_center_user(post_vars): # first determine if we need to create a new TestCenterUser, or if we are making any update # to an existing TestCenterUser. - username=post_vars['username'] + username = post_vars['username'] user = User.objects.get(username=username) - + course_id = post_vars['course_id'] + course = (course_from_id(course_id)) # assume it will be found.... + needs_saving = False try: testcenter_user = TestCenterUser.objects.get(user=user) @@ -651,9 +655,8 @@ def _do_create_or_update_test_center_user(post_vars): for fieldname in TestCenterUser.user_provided_fields()]) if needs_updating: - # TODO: what do we set a timestamp to, in order to get now()? + # leave user and client_candidate_id as before testcenter_user.user_updated_at = datetime.datetime.now() - # Now do the update: for fieldname in TestCenterUser.user_provided_fields(): testcenter_user.__setattr__(fieldname, post_vars[fieldname]) needs_saving = True @@ -661,7 +664,6 @@ def _do_create_or_update_test_center_user(post_vars): except TestCenterUser.DoesNotExist: # did not find the TestCenterUser, so create a new one testcenter_user = TestCenterUser(user=user) - # testcenter_user.user = user for fieldname in TestCenterUser.user_provided_fields(): testcenter_user.__setattr__(fieldname, post_vars[fieldname]) # testcenter_user.candidate_id remains unset @@ -670,31 +672,43 @@ def _do_create_or_update_test_center_user(post_vars): needs_saving = True # additional validation occurs at save time, so handle exceptions - # TODO: Rearrange so that if part of the process fails, the whole process fails. - # Right now, we can have e.g. no registration e-mail sent out and a zombie account if needs_saving: try: testcenter_user.save() - except IntegrityError: - js = {'success': False} - # TODO: Figure out the cause of the integrity error - if len(User.objects.filter(username=post_vars['username'])) > 0: - js['value'] = "An account with this username already exists." - js['field'] = 'username' - return HttpResponse(json.dumps(js)) + except IntegrityError, ie: + message = ie + context = {'course': course, + 'user': user, + 'message': message, + 'testcenteruser': testcenter_user, + } + return render_to_response('test_center_register.html', context) - if len(User.objects.filter(email=post_vars['email'])) > 0: - js['value'] = "An account with this e-mail already exists." - js['field'] = 'email' - return HttpResponse(json.dumps(js)) - - raise - - + # create and save the registration: registration = TestCenterRegistration(testcenter_user = testcenter_user) - # registration.register(user) registration.course_id = post_vars['course_id'] registration.accommodation_request = post_vars['accommodations'] + exam_info = course.testcenter_info + registration.exam_series_code = exam_info.get('Exam_Series_Code') + registration.eligibility_appointment_date_first = exam_info.get('First_Eligible_Appointment_Date') + registration.eligibility_appointment_date_last = exam_info.get('Last_Eligible_Appointment_Date') + # accommodation_code remains blank for now, along with Pearson confirmation + registration.user_updated_at = datetime.datetime.now() + + # "client_authorization_id" is the client's unique identifier for the authorization. + # This must be present for an update or delete to be sent to Pearson. + registration.client_authorization_id = "1" + try: + registration.save() + except IntegrityError, ie: + message = ie + context = {'course': course, + 'user': user, + 'message': message, + 'testcenteruser': testcenter_user, + } + return render_to_response('test_center_register.html', context) + return (user, testcenter_user, registration) @@ -759,11 +773,12 @@ def create_test_registration(request, post_override=None): js['value'] = 'Could not send accommodation e-mail.' return HttpResponse(json.dumps(js)) - + # TODO: enable appropriate stat # statsd.increment("common.student.account_created") - js = {'success': True} - return HttpResponse(json.dumps(js), mimetype="application/json") +# js = {'success': True} +# return HttpResponse(json.dumps(js), mimetype="application/json") + return HttpResponseRedirect('/dashboard') def get_random_post_override(): """ diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index a4cf87b333..715b263b59 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -315,7 +315,7 @@ class CourseDescriptor(SequenceDescriptor): Returns None if no url specified. """ return self.metadata.get('end_of_course_survey_url') - + @property def testcenter_info(self): """ @@ -324,10 +324,17 @@ class CourseDescriptor(SequenceDescriptor): TODO: decide if we expect this entry to be a single test, or if multiple tests are possible per course. - Returns None if no testcenter info specified. + For now we expect this entry to be a single test. + + Returns None if no testcenter info specified, or if no exam is included. """ - return self.metadata.get('testcenter_info') - + info = self.metadata.get('testcenter_info') + if info is None or len(info) == 0: + return None; + else: + return info.values()[0] + + @property def title(self): return self.display_name diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index c7a8df077a..13e393180a 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -3,6 +3,7 @@ from courseware.courses import course_image_url, get_course_about_section from courseware.access import has_access from certificates.models import CertificateStatuses + from student.models import get_testcenter_registrations_for_user_and_course %> <%inherit file="main.html" /> @@ -222,20 +223,26 @@ <% testcenter_info = course.testcenter_info + testcenter_register_target = reverse('begin_test_registration', args=[course.id]) %> % if testcenter_info is not None: - - <% - testcenter_register_target = reverse('begin_test_registration', args=[course.id]) - %> + + + <% + registrations = get_testcenter_registrations_for_user_and_course(user, course.id) + %> + % if len(registrations) == 0:
Register for Pearson exam

Registration for the Pearson exam is now open.

- + % else:
-

Your registration for the Pearson exam is pending. Within a few days, you should receive a confirmation number, which can be used to schedule your exam.

+

Your + registration for the Pearson exam + is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.

@@ -244,6 +251,7 @@

Registration number: edx00015879548

Write this down! You’ll need it to schedule your exam.

+ % endif % endif diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index c71c40610d..51b271dfc3 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -71,12 +71,9 @@ <% - testcenter_info = course.testcenter_info + exam_info = course.testcenter_info %> - % if testcenter_info is not None: - <% - exam_info = testcenter_info.get('Final_Exam') - %> + % if exam_info is not None:

Exam Series Code: ${exam_info.get('Exam_Series_Code')}

First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

From d5bd2313c1baaa395bda60868f467577387a4d8b Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 20 Dec 2012 04:16:00 -0500 Subject: [PATCH 008/541] fix migrations, removing one field and making another nullable. Update registration page to allow for editing only demographics. --- ...022_add_more_fields_to_test_center_user.py | 15 ++-- common/djangoapps/student/models.py | 13 ++-- common/djangoapps/student/views.py | 75 ++++++++++-------- lms/templates/dashboard.html | 3 +- lms/templates/test_center_register.html | 78 ++++++++++++++++--- 5 files changed, 128 insertions(+), 56 deletions(-) diff --git a/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py index 4a6481f80c..1bffec2213 100644 --- a/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py +++ b/common/djangoapps/student/migrations/0022_add_more_fields_to_test_center_user.py @@ -13,8 +13,8 @@ class Migration(SchemaMigration): self.gf('django.db.models.fields.CharField')(default='', max_length=20, blank=True), keep_default=False) - # Adding field 'TestCenterUser.confirmed_at' - db.add_column('student_testcenteruser', 'confirmed_at', + # Adding field 'TestCenterUser.uploaded_at' + db.add_column('student_testcenteruser', 'uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True), keep_default=False) @@ -31,14 +31,13 @@ class Migration(SchemaMigration): ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), ('user_updated_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), - ('client_authorization_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=20, db_index=True)), ('exam_series_code', self.gf('django.db.models.fields.CharField')(max_length=15, db_index=True)), ('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)), ('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)), ('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), ('accommodation_request', self.gf('django.db.models.fields.CharField')(max_length=1024, blank=True)), ('upload_status', self.gf('django.db.models.fields.CharField')(max_length=20, blank=True)), - ('confirmed_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), ('upload_error_message', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)), )) db.send_create_signal('student', ['TestCenterRegistration']) @@ -51,8 +50,8 @@ class Migration(SchemaMigration): # Deleting field 'TestCenterUser.upload_status' db.delete_column('student_testcenteruser', 'upload_status') - # Deleting field 'TestCenterUser.confirmed_at' - db.delete_column('student_testcenteruser', 'confirmed_at') + # Deleting field 'TestCenterUser.uploaded_at' + db.delete_column('student_testcenteruser', 'uploaded_at') # Deleting field 'TestCenterUser.upload_error_message' db.delete_column('student_testcenteruser', 'upload_error_message') @@ -127,7 +126,7 @@ class Migration(SchemaMigration): 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 'accommodation_request': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), - 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), 'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), @@ -149,7 +148,7 @@ class Migration(SchemaMigration): 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6b73c2319f..716c472330 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -194,7 +194,7 @@ class TestCenterUser(models.Model): # Confirmation upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' - confirmed_at = models.DateTimeField(null=True, db_index=True) + uploaded_at = models.DateTimeField(null=True, db_index=True) upload_error_message = models.CharField(max_length=512, blank=True) @staticmethod @@ -236,7 +236,7 @@ class TestCenterRegistration(models.Model): user_updated_at = models.DateTimeField(db_index=True) # "client_authorization_id" is the client's unique identifier for the authorization. # This must be present for an update or delete to be sent to Pearson. - client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True) + # client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True) # information about the test, from the course policy: exam_series_code = models.CharField(max_length=15, db_index=True) @@ -250,7 +250,7 @@ class TestCenterRegistration(models.Model): # Confirmation upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' - confirmed_at = models.DateTimeField(db_index=True) + uploaded_at = models.DateTimeField(null=True, db_index=True) upload_error_message = models.CharField(max_length=512, blank=True) @property @@ -261,12 +261,15 @@ class TestCenterRegistration(models.Model): def client_candidate_id(self): return self.testcenter_user.client_candidate_id - + @property + def client_authorization_id(self): + # TODO: make this explicitly into a string object: + return self.id def get_testcenter_registrations_for_user_and_course(user, course_id): try: tcu = TestCenterUser.objects.get(user=user) - except User.DoesNotExist: + except TestCenterUser.DoesNotExist: return [] return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 4d5728792e..62943478a0 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -10,6 +10,7 @@ import sys import urllib import uuid + from django.conf import settings from django.contrib.auth import logout, authenticate, login from django.contrib.auth.forms import PasswordResetForm @@ -17,6 +18,7 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.core.context_processors import csrf from django.core.mail import send_mail +from django.core.urlresolvers import reverse from django.core.validators import validate_email, validate_slug, ValidationError from django.db import IntegrityError from django.http import HttpResponse, HttpResponseForbidden, Http404,\ @@ -29,7 +31,8 @@ from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt from student.models import (Registration, UserProfile, TestCenterUser, TestCenterRegistration, PendingNameChange, PendingEmailChange, - CourseEnrollment, unique_id_for_user) + CourseEnrollment, unique_id_for_user, + get_testcenter_registrations_for_user_and_course) from certificates.models import CertificateStatuses, certificate_status_for_student @@ -617,11 +620,7 @@ def begin_test_registration(request, course_id): log.error("User {0} enrolled in non-existent course {1}" .format(user.username, course_id)) - # TODO: placeholder for possible messages... message = "" - if not user.is_active: - message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) - context = {'course': course, 'user': user, 'message': message, @@ -676,7 +675,7 @@ def _do_create_or_update_test_center_user(post_vars): try: testcenter_user.save() except IntegrityError, ie: - message = ie + message = "%s" % ie context = {'course': course, 'user': user, 'message': message, @@ -685,31 +684,45 @@ def _do_create_or_update_test_center_user(post_vars): return render_to_response('test_center_register.html', context) # create and save the registration: - registration = TestCenterRegistration(testcenter_user = testcenter_user) - registration.course_id = post_vars['course_id'] - registration.accommodation_request = post_vars['accommodations'] - exam_info = course.testcenter_info - registration.exam_series_code = exam_info.get('Exam_Series_Code') - registration.eligibility_appointment_date_first = exam_info.get('First_Eligible_Appointment_Date') - registration.eligibility_appointment_date_last = exam_info.get('Last_Eligible_Appointment_Date') - # accommodation_code remains blank for now, along with Pearson confirmation - registration.user_updated_at = datetime.datetime.now() - - # "client_authorization_id" is the client's unique identifier for the authorization. - # This must be present for an update or delete to be sent to Pearson. - registration.client_authorization_id = "1" - try: - registration.save() - except IntegrityError, ie: - message = ie - context = {'course': course, - 'user': user, - 'message': message, - 'testcenteruser': testcenter_user, - } - return render_to_response('test_center_register.html', context) + needs_saving = False + registrations = get_testcenter_registrations_for_user_and_course(user, course.id) + # In future, this should check the exam series code of the registrations, if there + # were multiple. + if len(registrations) > 0: + registration = registrations[0] + # check to see if registration changed. Should check appointment dates too... + # And later should check changes in accommodation_code. + # But at the moment, we don't expect anything to cause this to change + # right now. + + else: + registration = TestCenterRegistration(testcenter_user = testcenter_user) + registration.course_id = post_vars['course_id'] + registration.accommodation_request = post_vars['accommodations'] + exam_info = course.testcenter_info + registration.exam_series_code = exam_info.get('Exam_Series_Code') + registration.eligibility_appointment_date_first = exam_info.get('First_Eligible_Appointment_Date') + registration.eligibility_appointment_date_last = exam_info.get('Last_Eligible_Appointment_Date') + # accommodation_code remains blank for now, along with Pearson confirmation + registration.user_updated_at = datetime.datetime.now() + needs_saving = True + + # "client_authorization_id" is the client's unique identifier for the authorization. + # This must be present for an update or delete to be sent to Pearson. + # Can we just use the id field of the registration? Lets... + + if needs_saving: + try: + registration.save() + except IntegrityError, ie: + message = "%s" % ie + context = {'course': course, + 'user': user, + 'message': message, + 'testcenteruser': testcenter_user, + } + return render_to_response('test_center_register.html', context) - return (user, testcenter_user, registration) @ensure_csrf_cookie @@ -778,7 +791,7 @@ def create_test_registration(request, post_override=None): # js = {'success': True} # return HttpResponse(json.dumps(js), mimetype="application/json") - return HttpResponseRedirect('/dashboard') + return HttpResponseRedirect(reverse('dashboard')) def get_random_post_override(): """ diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 13e393180a..1f1553d90c 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -234,8 +234,7 @@ %> % if len(registrations) == 0:
- Register for Pearson exam + Register for Pearson exam

Registration for the Pearson exam is now open.

% else: diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 51b271dfc3..8c99b4f8ef 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -3,6 +3,7 @@ from courseware.courses import course_image_url, get_course_about_section from courseware.access import has_access from certificates.models import CertificateStatuses + from student.models import get_testcenter_registrations_for_user_and_course %> <%inherit file="main.html" /> @@ -34,14 +35,7 @@ -
- - - %if message: -
- ${message} -
- %endif +
@@ -81,7 +75,63 @@
+ + <% + registrations = get_testcenter_registrations_for_user_and_course(user, course.id) + %> + + % if len(registrations) > 0: + <% + registration = registrations[0] + %> +
Already Registered
+

Here is the current state of your registration, for debugging purposes:

+ +
  • id: ${registration.id}
  • +
  • testcenter_user_id: ${registration.testcenter_user_id}
  • +
  • course_id: ${registration.course_id}
  • +
  • accommodation codes: ${registration.accommodation_code}
  • +
  • accommodation request: ${registration.accommodation_request}
  • +
  • created_at: ${registration.created_at}
  • +
  • updated_at: ${registration.updated_at}
  • +
  • user_updated_at: ${registration.user_updated_at}
  • +
  • upload_status: ${registration.upload_status}
  • +
  • upload_error_message: ${registration.upload_error_message}
  • +
    + + + + <% + regstatus = "registration pending acknowledgement by Pearson" + + if registration.upload_status == 'Accepted': + regstatus = "registration approved by Pearson" + elif registration.upload_status == 'Error': + regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message + elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': + regstatus = "pending approval of accommodation request" + %> +

    Current status: ${regstatus}

    + +

    The demographic information provided below was used to register + for the exam listed above. Changes to this information + may be submitted below.

    +
    + % else: +

    The demographic information must be provided below in order to register + for the exam listed above.

    + % endif + + + % if message: +
    +

    ${message}

    +
    + % endif +
    @@ -91,7 +141,6 @@ -
    @@ -150,15 +199,24 @@
    - +

    The following is included here just so it can be input within the form. But it + is not part of the demographics, and it is not something that can be changed once input.

    +
    + % if len(registrations) > 0: +
    + +
    + % else:
    + % endif +
    From 5119257f3e2ef7357b541ad18fa736f19b5e47f1 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Dec 2012 12:24:31 -0500 Subject: [PATCH 009/541] test center - fixed dashboard link font issue and started cleaning up markup in reg form - wip --- lms/static/sass/application.scss | 1 + lms/static/sass/multicourse/_dashboard.scss | 4 + lms/templates/test_center_register.html | 309 +++++++++----------- 3 files changed, 150 insertions(+), 164 deletions(-) diff --git a/lms/static/sass/application.scss b/lms/static/sass/application.scss index 944d3b2884..4e532cf30e 100644 --- a/lms/static/sass/application.scss +++ b/lms/static/sass/application.scss @@ -19,6 +19,7 @@ @import 'multicourse/home'; @import 'multicourse/dashboard'; +@import 'multicourse/testcenter-register'; @import 'multicourse/courses'; @import 'multicourse/course_about'; @import 'multicourse/jobs'; diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 249e8a0513..8383b01a54 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -418,6 +418,10 @@ font-size: 13px; margin: 0; + a { + font-family: $sans-serif; + } + .grade-value { font-size: 1.2rem; font-weight: bold; diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 8c99b4f8ef..f1b071d121 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -9,7 +9,7 @@ <%namespace name='static' file='static_content.html'/> -<%block name="title">Sign Up for Pearson VUE Test Center Proctoring +<%block name="title">Pearson VUE Test Center Proctoring - Sign Up <%block name="js_extra"> -
    +
    -
    -
    -
    -

    -
    -
    + - +
    +
    +

    + % if course.has_ended(): + Course Completed - ${course.end_date_text} + % elif course.has_started(): + Course Started - ${course.start_date_text} + % else: # hasn't started yet + Course Starts - ${course.start_date_text} + % endif +

    +

    ${get_course_about_section(course, 'university')}

    +

    ${course.number} ${course.title}

    +
    -
    -
    -

    - % if course.has_ended(): - Course Completed - ${course.end_date_text} - % elif course.has_started(): - Course Started - ${course.start_date_text} - % else: # hasn't started yet - Course Starts - ${course.start_date_text} - % endif -

    -

    ${get_course_about_section(course, 'university')}

    -

    ${course.number} ${course.title}

    -
    - - - <% - exam_info = course.testcenter_info - %> - % if exam_info is not None: -

    Exam Series Code: ${exam_info.get('Exam_Series_Code')}

    -

    First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

    -

    Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

    - % endif - -
    + + <% + exam_info = course.testcenter_info + %> + % if exam_info is not None: +

    Exam Series Code: ${exam_info.get('Exam_Series_Code')}

    +

    First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

    +

    Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

    + % endif +
    @@ -86,40 +78,41 @@ registration = registrations[0] %> -
    Already Registered
    -

    Here is the current state of your registration, for debugging purposes:

    - -
  • id: ${registration.id}
  • -
  • testcenter_user_id: ${registration.testcenter_user_id}
  • -
  • course_id: ${registration.course_id}
  • -
  • accommodation codes: ${registration.accommodation_code}
  • -
  • accommodation request: ${registration.accommodation_request}
  • -
  • created_at: ${registration.created_at}
  • -
  • updated_at: ${registration.updated_at}
  • -
  • user_updated_at: ${registration.user_updated_at}
  • -
  • upload_status: ${registration.upload_status}
  • -
  • upload_error_message: ${registration.upload_error_message}
  • -
    +
    Already Registered
    +

    Here is the current state of your registration, for debugging purposes:

    + +
  • id: ${registration.id}
  • +
  • testcenter_user_id: ${registration.testcenter_user_id}
  • +
  • course_id: ${registration.course_id}
  • +
  • accommodation codes: ${registration.accommodation_code}
  • +
  • accommodation request: ${registration.accommodation_request}
  • +
  • created_at: ${registration.created_at}
  • +
  • updated_at: ${registration.updated_at}
  • +
  • user_updated_at: ${registration.user_updated_at}
  • +
  • upload_status: ${registration.upload_status}
  • +
  • upload_error_message: ${registration.upload_error_message}
  • +
    + + + + <% + regstatus = "registration pending acknowledgement by Pearson" - - - <% - regstatus = "registration pending acknowledgement by Pearson" - - if registration.upload_status == 'Accepted': - regstatus = "registration approved by Pearson" - elif registration.upload_status == 'Error': - regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message - elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': - regstatus = "pending approval of accommodation request" - %> -

    Current status: ${regstatus}

    - -

    The demographic information provided below was used to register - for the exam listed above. Changes to this information - may be submitted below.

    -
    + if registration.upload_status == 'Accepted': + regstatus = "registration approved by Pearson" + elif registration.upload_status == 'Error': + regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message + elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': + regstatus = "pending approval of accommodation request" + %> +

    Current status: ${regstatus}

    + +

    The demographic information provided below was used to register + for the exam listed above. Changes to this information + may be submitted below.

    +
    + % else:

    The demographic information must be provided below in order to register for the exam listed above.

    @@ -127,106 +120,94 @@ % if message: -
    -

    ${message}

    -
    +
    +

    ${message}

    +
    % endif -
    -
    - - -
    - - - - - - -
    - - -
    - - -
    - - -
    - - -
    - - -
    -
    - -
    - - -
    - - -
    - - -
    - - - - - -
    - - - - -
    - -
    - - - - - - -
    - - - - -
    - - -
    -

    The following is included here just so it can be input within the form. But it - is not part of the demographics, and it is not something that can be changed once input.

    -
    -
    - - -
    - - % if len(registrations) > 0: -
    - -
    - % else: -
    - -
    - % endif + -
    + + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    - + +
    + + +
    + + +
    + + +
    + + + + + +
    + + + + +
    + +
    + + + + + + +
    + + + + +
    + + +
    +
    + +

    The following is included here just so it can be input within the form. But it is not part of the demographics, and it is not something that can be changed once input.

    + +
    + +
    + +% if len(registrations) > 0: +
    + +
    + % else: +
    + +
    + % endif + +
    From 76f6ee18772d905033d2ff65218f9ca8c3dd2588 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Dec 2012 13:00:08 -0500 Subject: [PATCH 010/541] test center - reorg of HTML - wip --- lms/templates/test_center_register.html | 132 ++++++++++++++---------- 1 file changed, 78 insertions(+), 54 deletions(-) diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index f1b071d121..8d5f7cb206 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -35,13 +35,17 @@ +<% +exam_info = course.testcenter_info +%> +
    -
    +

    % if course.has_ended(): @@ -57,66 +61,86 @@

    - <% - exam_info = course.testcenter_info - %> % if exam_info is not None:

    Exam Series Code: ${exam_info.get('Exam_Series_Code')}

    First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

    Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

    % endif + + + <% + registrations = get_testcenter_registrations_for_user_and_course(user, course.id) + %> + + % if len(registrations) > 0: + <% + registration = registrations[0] + %> + +
    Already Registered
    +

    Here is the current state of your registration, for debugging purposes:

    + +
  • id: ${registration.id}
  • +
  • testcenter_user_id: ${registration.testcenter_user_id}
  • +
  • course_id: ${registration.course_id}
  • +
  • accommodation codes: ${registration.accommodation_code}
  • +
  • accommodation request: ${registration.accommodation_request}
  • +
  • created_at: ${registration.created_at}
  • +
  • updated_at: ${registration.updated_at}
  • +
  • user_updated_at: ${registration.user_updated_at}
  • +
  • upload_status: ${registration.upload_status}
  • +
  • upload_error_message: ${registration.upload_error_message}
  • +
    + + + + <% + regstatus = "registration pending acknowledgement by Pearson" + + if registration.upload_status == 'Accepted': + regstatus = "registration approved by Pearson" + elif registration.upload_status == 'Error': + regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message + elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': + regstatus = "pending approval of accommodation request" + %> +

    Current status: ${regstatus}

    + +

    The demographic information provided below was used to register + for the exam listed above. Changes to this information + may be submitted below.

    +
    + + % else: +

    The demographic information must be provided below in order to register + for the exam listed above.

    + % endif
    - - - <% - registrations = get_testcenter_registrations_for_user_and_course(user, course.id) - %> - - % if len(registrations) > 0: - <% - registration = registrations[0] - %> - -
    Already Registered
    -

    Here is the current state of your registration, for debugging purposes:

    - -
  • id: ${registration.id}
  • -
  • testcenter_user_id: ${registration.testcenter_user_id}
  • -
  • course_id: ${registration.course_id}
  • -
  • accommodation codes: ${registration.accommodation_code}
  • -
  • accommodation request: ${registration.accommodation_request}
  • -
  • created_at: ${registration.created_at}
  • -
  • updated_at: ${registration.updated_at}
  • -
  • user_updated_at: ${registration.user_updated_at}
  • -
  • upload_status: ${registration.upload_status}
  • -
  • upload_error_message: ${registration.upload_error_message}
  • -
    - - - - <% - regstatus = "registration pending acknowledgement by Pearson" - - if registration.upload_status == 'Accepted': - regstatus = "registration approved by Pearson" - elif registration.upload_status == 'Error': - regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message - elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': - regstatus = "pending approval of accommodation request" - %> -

    Current status: ${regstatus}

    - -

    The demographic information provided below was used to register - for the exam listed above. Changes to this information - may be submitted below.

    -
    - % else: -

    The demographic information must be provided below in order to register - for the exam listed above.

    - % endif +
    +
    +
    +

    ${get_course_about_section(course, 'university')} ${course.number} ${course.title}

    +

    Register for a Pearson VUE Proctored Exam

    +
    + + + % if course.has_ended(): + Course Completed: ${course.end_date_text} + % elif course.has_started(): + Course Started: ${course.start_date_text} + % else: # hasn't started yet + Course Starts: ${course.start_date_text} + % endif + +
    + +
    + +
    +
    % if message: From 2788ad8629d8f0371339543df5d8c3e921a73882 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 21 Dec 2012 01:39:18 -0500 Subject: [PATCH 011/541] switch back to returning json to test_center_register template, and enable jquery handling in template. --- common/djangoapps/student/views.py | 40 ++++++++++++------------- lms/templates/test_center_register.html | 16 ++++------ 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 62943478a0..29d8a206cb 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -675,13 +675,18 @@ def _do_create_or_update_test_center_user(post_vars): try: testcenter_user.save() except IntegrityError, ie: - message = "%s" % ie - context = {'course': course, - 'user': user, - 'message': message, - 'testcenteruser': testcenter_user, - } - return render_to_response('test_center_register.html', context) + js = {'success': False} + error_msg = unicode(ie); + # attempt to find a field name to signal + for fieldname in TestCenterUser.user_provided_fields(): + if error_msg.find(fieldname) >= 0: + js['value'] = error_msg + js['field'] = fieldname + return HttpResponse(json.dumps(js)) + # otherwise just return the error message + js['value'] = error_msg + js['field'] = "General Error" + return HttpResponse(json.dumps(js)) # create and save the registration: needs_saving = False @@ -715,13 +720,7 @@ def _do_create_or_update_test_center_user(post_vars): try: registration.save() except IntegrityError, ie: - message = "%s" % ie - context = {'course': course, - 'user': user, - 'message': message, - 'testcenteruser': testcenter_user, - } - return render_to_response('test_center_register.html', context) + raise return (user, testcenter_user, registration) @@ -743,14 +742,14 @@ def create_test_registration(request, post_override=None): js['field'] = a return HttpResponse(json.dumps(js)) - # Confirm appropriate fields are there. + # Confirm appropriate fields are filled in with something for now for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: if len(post_vars[a]) < 2: error_str = {'first_name': 'First name must be minimum of two characters long.', 'last_name': 'Last name must be minimum of two characters long.', - 'address_1': 'Last name must be minimum of two characters long.', - 'city': 'Last name must be minimum of two characters long.', - 'country': 'Last name must be minimum of two characters long.', + 'address_1': 'Address must be minimum of two characters long.', + 'city': 'City must be minimum of two characters long.', + 'country': 'Country must be minimum of two characters long.', } js['value'] = error_str[a] js['field'] = a @@ -789,9 +788,8 @@ def create_test_registration(request, post_override=None): # TODO: enable appropriate stat # statsd.increment("common.student.account_created") -# js = {'success': True} -# return HttpResponse(json.dumps(js), mimetype="application/json") - return HttpResponseRedirect(reverse('dashboard')) + js = {'success': True} + return HttpResponse(json.dumps(js), mimetype="application/json") def get_random_post_override(): """ diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 8c99b4f8ef..b4d47bf487 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -15,19 +15,13 @@ From e170afa9276df4ae09e1508af18f17affc5ac4a1 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 21 Dec 2012 13:23:20 -0500 Subject: [PATCH 014/541] test center registration - forgot to add/commit corresponding SASS file --- .../multicourse/_testcenter-register.scss | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 lms/static/sass/multicourse/_testcenter-register.scss diff --git a/lms/static/sass/multicourse/_testcenter-register.scss b/lms/static/sass/multicourse/_testcenter-register.scss new file mode 100644 index 0000000000..9286155c48 --- /dev/null +++ b/lms/static/sass/multicourse/_testcenter-register.scss @@ -0,0 +1,412 @@ +// ========== + +$baseline: 20px; +$yellow: rgb(255, 235, 169); +$red: rgb(178, 6, 16); + +// ========== + +.testcenter-register { + @include clearfix; + padding: 60px 0px 120px; + + // basic layout + .introduction { + width: flex-grid(12); + } + + .message-status-registration { + width: flex-grid(12); + } + + .content, aside { + @include box-sizing(border-box); + } + + .content { + margin-right: flex-gutter(); + width: flex-grid(8); + float: left; + } + + aside { + margin: 0; + width: flex-grid(4); + float: left; + } + + // introduction + .introduction { + + header { + + h2 { + margin: 0; + font-family: $sans-serif; + font-size: 16px; + color: $lighter-base-font-color; + } + + h1 { + font-family: $sans-serif; + font-size: 34px; + text-align: left; + } + } + } + + // content + .content { + background: rgb(255,255,255); + } + + // form + .form-fields-primary, .form-fields-secondary { + border-bottom: 1px solid rgba(0,0,0,0.25); + @include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.1)); + } + + form { + border: 1px solid rgb(216, 223, 230); + @include border-radius(3px); + @include box-shadow(0 1px 2px 0 rgba(0,0,0, 0.2)); + + .instructions { + margin: 0; + padding: ($baseline*1.5) ($baseline*1.5) 0 ($baseline*1.5); + font-family: $sans-serif; + font-size: 14px; + } + + fieldset { + border-bottom: 1px solid rgba(216, 223, 230, 0.50); + padding: ($baseline*1.5); + } + + .form-actions { + padding: ($baseline*1.5); + + button[type="submit"] { + display: block; + width: 100%; + @include button(simple, $blue); + @include box-sizing(border-box); + @include border-radius(3px); + font: bold 15px/1.6rem $sans-serif; + letter-spacing: 0; + padding: ($baseline*0.75) $baseline; + text-align: center; + } + } + + .list-input { + margin: 0; + padding: 0; + list-style: none; + + .field { + border-bottom: 1px dotted rgba(216, 223, 230, 0.5); + margin: 0 0 $baseline 0; + padding: 0 0 $baseline 0; + + &:last-child { + border: none; + margin-bottom: 0; + padding-bottom: 0; + } + + &.disabled { + color: rgba(0,0,0,.25); + + label { + color: rgba(0,0,0,.25); + + &:after { + content: "(Disabled Currently)"; + margin-left: ($baseline/4); + } + } + + textarea, input { + background: rgb(255,255,255); + color: rgba(0,0,0,.25); + } + } + + &.error { + + label { + color: $red; + } + + input, textarea { + border-color: tint($red,50%); + } + } + + &.required { + + label { + font-weight: bold; + } + + label:after { + margin-left: ($baseline/4); + content: "*"; + } + } + + label, input, textarea { + display: block; + font-family: $sans-serif; + font-style: normal; + } + + label { + margin: 0 0 ($baseline/4) 0; + @include transition(color, 0.15s, ease-in-out); + + &.is-focused { + color: $blue; + } + } + + input, textarea { + width: 100%; + padding: $baseline ($baseline*.75); + + &.long { + width: 100%; + } + + &.short { + width: 25%; + } + } + + textarea.long { + height: ($baseline*5); + } + } + + .field-group { + @include clearfix(); + border-bottom: 1px dotted rgba(216, 223, 230, 0.5); + margin: 0 0 $baseline 0; + padding: 0 0 $baseline 0; + + .field { + display: block; + float: left; + border-bottom: none; + margin: 0 ($baseline) 0 0; + padding-bottom: 0; + + input, textarea { + width: 100%; + } + } + + &.addresses { + + .field { + width: 45%; + } + } + + &.postal { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; + + } + + &.phoneinfo { + + } + } + } + } + + + // aside + aside { + padding-left: $baseline; + + .message-status { + @include border-radius(3px); + margin: 0 0 ($baseline*2) 0; + padding: $baseline; + border: 1px solid #ccc; + background: tint($yellow,85%); + + p { + margin: 0 0 ($baseline/4) 0; + padding: 0; + font-size: 13px; + font-family: $sans-serif; + } + + .label, .value { + display: block; + } + + .label { + margin-right: ($baseline/4); + text-transform: uppercase; + letter-spacing: 1px; + } + + .value { + color: rgba(0,0,0,0.9); + font-size: 14px; + } + + .registration-status { + margin: 0 0 ($baseline/2) 0; + + .label { + margin: 0 0 ($baseline/2) 0; + color: #ccc; + } + } + + .registration-number { + + .label { + text-transform: none; + letter-spacing: 0; + } + + .label, .value { + display: inline-block + } + } + + .message-copy { + margin: 0; + color: rgba(0,0,0,0.65); + } + } + + .registration-accepted { + + .message-copy { + margin: 0 0 ($baseline/2) 0; + } + + .exam-button { + @include button(simple, $pink); + display: block; + padding: ($baseline/2) $baseline; + font-size: 13px; + font-weight: bold; + + &:hover { + text-decoration: none; + } + } + } + + .details { + border-bottom: 1px solid rgba(216, 223, 230, 0.5); + margin: 0 0 $baseline 0; + padding: 0 0 $baseline 0; + font-family: $sans-serif; + font-size: 14px; + + &:last-child { + border: none; + margin-bottom: 0; + padding-bottom: 0; + } + + h4 { + margin: 0 0 ($baseline/2) 0; + font-family: $sans-serif; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #ccc; + } + + .label, .value { + display: inline-block; + } + + .label { + color: rgba(0,0,0,.65); + margin-right: ($baseline/2); + } + + .value { + color: rgb(0,0,0); + } + } + + .details-course { + + } + + .details-registration { + + ul { + margin: 0; + padding: 0; + list-style: none; + + li { + margin: 0 0 ($baseline/4) 0; + } + } + } + } + + // status messages + .message { + @include border-radius(3px); + display: none; + margin: $baseline 0; + padding: ($baseline/2) $baseline; + + &.is-shown { + display: block; + } + + .message-copy { + font-family: $sans-serif; + font-size: 14px; + } + + // submission error + &.submission-error { + border: 1px solid tint($red,85%); + background: tint($red,95%); + + p { + color: $red; + } + } + + // submission success + &.submission-saved { + border: 1px solid tint($blue,85%); + background: tint($blue,95%); + + .message-copy { + color: $blue; + } + } + } + + + // hidden + .is-hidden { + display: none; + } + + // temp + .output-raw { + display: none; + } +} \ No newline at end of file From 4abd9cd3d437a149973655af10ed49654efb57dd Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 2 Jan 2013 18:56:25 -0500 Subject: [PATCH 015/541] get closer to working again --- common/djangoapps/student/models.py | 57 ++++++++++++++++- common/djangoapps/student/views.py | 81 ++++++++++++++----------- lms/templates/test_center_register.html | 76 ++++++++++++++--------- 3 files changed, 148 insertions(+), 66 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 716c472330..88b3c3cd80 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -47,6 +47,7 @@ from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver +from django.forms import ModelForm import comment_client as cc from django_comment_client.models import Role @@ -194,7 +195,7 @@ class TestCenterUser(models.Model): # Confirmation upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' - uploaded_at = models.DateTimeField(null=True, db_index=True) + uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True) upload_error_message = models.CharField(max_length=512, blank=True) @staticmethod @@ -206,7 +207,56 @@ class TestCenterUser(models.Model): @property def email(self): return self.user.email + + def needs_update(self, dict): +# needs_updating = any([__getattribute__(fieldname) != dict[fieldname] +# for fieldname in TestCenterUser.user_provided_fields()]) + for fieldname in TestCenterUser.user_provided_fields(): + if self.__getattribute__(fieldname) != dict[fieldname]: + return True + + return False + + def update(self, dict): + # leave user and client_candidate_id as before + self.user_updated_at = datetime.now() + for fieldname in TestCenterUser.user_provided_fields(): + self.__setattr__(fieldname, dict[fieldname]) + @staticmethod + def create(user, dict): + testcenter_user = TestCenterUser(user=user) + testcenter_user.update(dict) + # testcenter_user.candidate_id remains unset + # TODO: assign an ID of our own: + testcenter_user.client_candidate_id = 'edx' + '123456' # some unique value + + +class TestCenterUserForm(ModelForm): + class Meta: + model = TestCenterUser + fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', + 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country', + 'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name') + + + + + + +ACCOMODATION_CODES = ( + ('EQPMNT', 'Equipment'), + ('ET12ET', 'Extra Time - 1/2 Exam Time'), + ('ET30MN', 'Extra Time - 30 Minutes'), + ('ETDBTM', 'Extra Time - Double Time'), + ('SEPRMM', 'Separate Room'), + ('SRREAD', 'Separate Room & Reader'), + ('SRRERC', 'Separate Room & Reader/Recorder'), + ('SRRECR', 'Separate Room & Recorder'), + ('SRSEAN', 'Separate Room & Service Animal'), + ('SRSGNR', 'Separate Room & Sign Lang Interp'), + ) + class TestCenterRegistration(models.Model): """ This is our representation of a user's registration for in-person testing, @@ -242,7 +292,8 @@ class TestCenterRegistration(models.Model): exam_series_code = models.CharField(max_length=15, db_index=True) eligibility_appointment_date_first = models.DateField(db_index=True) eligibility_appointment_date_last = models.DateField(db_index=True) - # TODO: this should be an enumeration: + + # this is really a list of codes, using an '*' as a delimiter. accommodation_code = models.CharField(max_length=64, blank=True) # store the original text of the accommodation request. @@ -250,7 +301,7 @@ class TestCenterRegistration(models.Model): # Confirmation upload_status = models.CharField(max_length=20, blank=True) # 'Error' or 'Accepted' - uploaded_at = models.DateTimeField(null=True, db_index=True) + uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True) upload_error_message = models.CharField(max_length=512, blank=True) @property diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 29d8a206cb..47992554f2 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -29,7 +29,7 @@ from bs4 import BeautifulSoup from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt -from student.models import (Registration, UserProfile, TestCenterUser, TestCenterRegistration, +from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm, TestCenterRegistration, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user, get_testcenter_registrations_for_user_and_course) @@ -650,29 +650,41 @@ def _do_create_or_update_test_center_user(post_vars): try: testcenter_user = TestCenterUser.objects.get(user=user) # found a TestCenterUser, so check to see if it has changed - needs_updating = any([testcenter_user.__getattribute__(fieldname) != post_vars[fieldname] - for fieldname in TestCenterUser.user_provided_fields()]) - +# needs_updating = any([testcenter_user.__getattribute__(fieldname) != post_vars[fieldname] +# for fieldname in TestCenterUser.user_provided_fields()]) + needs_updating = testcenter_user.needs_update(post_vars) if needs_updating: - # leave user and client_candidate_id as before - testcenter_user.user_updated_at = datetime.datetime.now() - for fieldname in TestCenterUser.user_provided_fields(): - testcenter_user.__setattr__(fieldname, post_vars[fieldname]) +# # leave user and client_candidate_id as before +# testcenter_user.user_updated_at = datetime.datetime.now() +# for fieldname in TestCenterUser.user_provided_fields(): +# testcenter_user.__setattr__(fieldname, post_vars[fieldname]) + testcenter_user.update(post_vars) needs_saving = True except TestCenterUser.DoesNotExist: # did not find the TestCenterUser, so create a new one - testcenter_user = TestCenterUser(user=user) - for fieldname in TestCenterUser.user_provided_fields(): - testcenter_user.__setattr__(fieldname, post_vars[fieldname]) - # testcenter_user.candidate_id remains unset - testcenter_user.client_candidate_id = 'edx' + '123456' # some unique value - testcenter_user.user_updated_at = datetime.datetime.now() + testcenter_user = TestCenterUser.create(user, post_vars) +# testcenter_user = TestCenterUser(user=user) +# testcenter_user.update(post_vars) +## for fieldname in TestCenterUser.user_provided_fields(): +## testcenter_user.__setattr__(fieldname, post_vars[fieldname]) +# # testcenter_user.candidate_id remains unset +# testcenter_user.client_candidate_id = 'edx' + '123456' # some unique value +## testcenter_user.user_updated_at = datetime.datetime.now() needs_saving = True - # additional validation occurs at save time, so handle exceptions if needs_saving: try: + # first perform validation on the user information + # using a Django Form. + form = TestCenterUserForm(testcenter_user) + if not form.is_valid(): + response_data = {'success': False} + # return a list of errors... + response_data['field_errors'] = form.errors + response_data['non_field_errors'] = form.non_field_errors() + return HttpResponse(json.dumps(response_data)) + testcenter_user.save() except IntegrityError, ie: js = {'success': False} @@ -728,7 +740,7 @@ def _do_create_or_update_test_center_user(post_vars): def create_test_registration(request, post_override=None): ''' JSON call to create test registration. - Used by form in test_center_register_modal.html, which is included + Used by form in test_center_register.html, which is called from into dashboard.html ''' js = {'success': False} @@ -736,24 +748,24 @@ def create_test_registration(request, post_override=None): post_vars = post_override if post_override else request.POST # Confirm we have a properly formed request - for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: - if a not in post_vars: - js['value'] = "Error (401 {field}). E-mail us.".format(field=a) - js['field'] = a - return HttpResponse(json.dumps(js)) - - # Confirm appropriate fields are filled in with something for now - for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: - if len(post_vars[a]) < 2: - error_str = {'first_name': 'First name must be minimum of two characters long.', - 'last_name': 'Last name must be minimum of two characters long.', - 'address_1': 'Address must be minimum of two characters long.', - 'city': 'City must be minimum of two characters long.', - 'country': 'Country must be minimum of two characters long.', - } - js['value'] = error_str[a] - js['field'] = a - return HttpResponse(json.dumps(js)) +# for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: +# if a not in post_vars: +# js['value'] = "Error (401 {field}). E-mail us.".format(field=a) +# js['field'] = a +# return HttpResponse(json.dumps(js)) +# +# # Confirm appropriate fields are filled in with something for now +# for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: +# if len(post_vars[a]) < 2: +# error_str = {'first_name': 'First name must be minimum of two characters long.', +# 'last_name': 'Last name must be minimum of two characters long.', +# 'address_1': 'Address must be minimum of two characters long.', +# 'city': 'City must be minimum of two characters long.', +# 'country': 'Country must be minimum of two characters long.', +# } +# js['value'] = error_str[a] +# js['field'] = a +# return HttpResponse(json.dumps(js)) # Once the test_center_user information has been validated, create the entries: ret = _do_create_or_update_test_center_user(post_vars) @@ -791,6 +803,7 @@ def create_test_registration(request, post_override=None): js = {'success': True} return HttpResponse(json.dumps(js), mimetype="application/json") + def get_random_post_override(): """ Return a dictionary suitable for passing to post_vars of _do_create_account or post_override diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 0860adfe6c..d6a38e1bb4 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -45,6 +45,13 @@ exam_info = course.testcenter_info If the user has already registered in the past for a test center, then also display their ID. --> + + <% + registrations = get_testcenter_registrations_for_user_and_course(user, course.id) + %> + +

    @@ -67,12 +74,6 @@ exam_info = course.testcenter_info

    Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

    % endif - - <% - registrations = get_testcenter_registrations_for_user_and_course(user, course.id) - %> - % if len(registrations) > 0: <% registration = registrations[0] @@ -161,14 +162,14 @@ exam_info = course.testcenter_info % if len(registrations) > 0:

    - Please complete the following form to update your demographic information used in your Pearson VUE Porctored Exam. Required fields are noted by bold text and an asterisk (*). + Please complete the following form to update your demographic information used in your Pearson VUE Proctored Exam. Required fields are noted by bold text and an asterisk (*).

    % else:

    - Please provide the following demographic information to register for a Pearson VUE Porctored Exam. Required fields are noted by bold text and an asterisk (*). + Please provide the following demographic information to register for a Pearson VUE Proctored Exam. Required fields are noted by bold text and an asterisk (*).

    % endif - + @@ -181,24 +182,24 @@ exam_info = course.testcenter_info
    1. - +
    2. - +
    3. - +
    4. - +
    5. - +
    @@ -209,30 +210,34 @@ exam_info = course.testcenter_info
    1. - +
    2. - +
      - +
    3. +
      + + +
      - +
      - +
      - +
    @@ -245,30 +250,30 @@ exam_info = course.testcenter_info
  • - +
    - +
    - +
  • - +
    - +
  • - +
  • @@ -312,38 +317,51 @@ exam_info = course.testcenter_info
    -
    \ No newline at end of file +
    From c76f37050a4b711de5a4bceea6a490799089ed18 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 3 Jan 2013 03:22:37 -0500 Subject: [PATCH 016/541] introduce form to template --- common/djangoapps/student/views.py | 154 ++++++++++++------------ lms/templates/test_center_register.html | 128 +++++--------------- 2 files changed, 103 insertions(+), 179 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 47992554f2..202bfefac5 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -39,7 +39,6 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud from xmodule.course_module import CourseDescriptor from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError #from datetime import date from collections import namedtuple @@ -209,14 +208,6 @@ def _cert_info(user, course, cert_status): def dashboard(request): user = request.user enrollments = CourseEnrollment.objects.filter(user=user) - - # we want to populate the registration page with the relevant information, - # if it already exists. Create an empty object otherwise. -# try: -# testcenteruser = TestCenterUser.objects.get(user=user) -# except TestCenterUser.DoesNotExist: -# testcenteruser = TestCenterUser() -# testcenteruser.user = user # Build our courses list for the user, but ignore any courses that no longer # exist (because the course IDs have changed). Still, we don't delete those @@ -603,9 +594,27 @@ def create_account(request, post_override=None): @login_required @ensure_csrf_cookie -def begin_test_registration(request, course_id): - user = request.user - +def begin_test_registration(request, course_id, form=None, message=''): + user = request.user + + try: + course = (course_from_id(course_id)) + except ItemNotFoundError: + log.error("User {0} enrolled in non-existent course {1}" + .format(user.username, course_id)) + + # get the exam to be registered for: + # (For now, we just assume there is one at most.) + exam_info = course.testcenter_info + + # figure out if the user is already registered for this exam: + # (Again, for now we assume that any registration that exists is for this exam.) + registrations = get_testcenter_registrations_for_user_and_course(user, course_id) + if len(registrations) > 0: + registration = registrations[0] + else: + registration = None + # we want to populate the registration page with the relevant information, # if it already exists. Create an empty object otherwise. try: @@ -613,31 +622,31 @@ def begin_test_registration(request, course_id): except TestCenterUser.DoesNotExist: testcenteruser = TestCenterUser() testcenteruser.user = user - - try: - course = (course_from_id(course_id)) - except ItemNotFoundError: - log.error("User {0} enrolled in non-existent course {1}" - .format(user.username, course_id)) - message = "" + if form is None: + form = TestCenterUserForm(instance=testcenteruser) + context = {'course': course, 'user': user, 'message': message, 'testcenteruser': testcenteruser, + 'registration': registration, + 'form': form, + 'exam_info': exam_info, } return render_to_response('test_center_register.html', context) +@ensure_csrf_cookie +def create_test_registration(request, post_override=None): + ''' + JSON call to create test registration. + Used by form in test_center_register.html, which is called from + into dashboard.html + ''' + # js = {'success': False} -def _do_create_or_update_test_center_user(post_vars): - """ - Given cleaned post variables, create the TestCenterUser and UserProfile objects, as well as the - registration for this user. - - Returns a tuple (User, UserProfile, TestCenterUser). - - """ + post_vars = post_override if post_override else request.POST # first determine if we need to create a new TestCenterUser, or if we are making any update # to an existing TestCenterUser. @@ -646,46 +655,43 @@ def _do_create_or_update_test_center_user(post_vars): course_id = post_vars['course_id'] course = (course_from_id(course_id)) # assume it will be found.... - needs_saving = False +# needs_saving = False try: testcenter_user = TestCenterUser.objects.get(user=user) - # found a TestCenterUser, so check to see if it has changed -# needs_updating = any([testcenter_user.__getattribute__(fieldname) != post_vars[fieldname] -# for fieldname in TestCenterUser.user_provided_fields()]) - needs_updating = testcenter_user.needs_update(post_vars) - if needs_updating: -# # leave user and client_candidate_id as before -# testcenter_user.user_updated_at = datetime.datetime.now() -# for fieldname in TestCenterUser.user_provided_fields(): -# testcenter_user.__setattr__(fieldname, post_vars[fieldname]) - testcenter_user.update(post_vars) - needs_saving = True - except TestCenterUser.DoesNotExist: - # did not find the TestCenterUser, so create a new one - testcenter_user = TestCenterUser.create(user, post_vars) -# testcenter_user = TestCenterUser(user=user) -# testcenter_user.update(post_vars) -## for fieldname in TestCenterUser.user_provided_fields(): -## testcenter_user.__setattr__(fieldname, post_vars[fieldname]) -# # testcenter_user.candidate_id remains unset -# testcenter_user.client_candidate_id = 'edx' + '123456' # some unique value -## testcenter_user.user_updated_at = datetime.datetime.now() - needs_saving = True + testcenter_user = TestCenterUser(user=user) - if needs_saving: + needs_updating = testcenter_user.needs_update(post_vars) +# if needs_updating: +# testcenter_user.update(post_vars) +# needs_saving = True + + # except TestCenterUser.DoesNotExist: + # did not find the TestCenterUser, so create a new one +# testcenter_user = TestCenterUser.create(user, post_vars) +# needs_saving = True + + # perform validation: + if needs_updating: try: # first perform validation on the user information # using a Django Form. - form = TestCenterUserForm(testcenter_user) + form = TestCenterUserForm(instance=testcenter_user, data=post_vars) if not form.is_valid(): - response_data = {'success': False} - # return a list of errors... - response_data['field_errors'] = form.errors - response_data['non_field_errors'] = form.non_field_errors() - return HttpResponse(json.dumps(response_data)) + return begin_test_registration(request, course_id, form, 'failed to validate') +# response_data = {'success': False} +# # return a list of errors... +# response_data['field_errors'] = form.errors +# response_data['non_field_errors'] = form.non_field_errors() +# return HttpResponse(json.dumps(response_data)) + + new_user = form.save(commit=False) + # create additional values here: + new_user.user_updated_at = datetime.datetime.now() + # TODO: create client value.... + new_user.save() - testcenter_user.save() + # testcenter_user.save() except IntegrityError, ie: js = {'success': False} error_msg = unicode(ie); @@ -715,7 +721,7 @@ def _do_create_or_update_test_center_user(post_vars): else: registration = TestCenterRegistration(testcenter_user = testcenter_user) registration.course_id = post_vars['course_id'] - registration.accommodation_request = post_vars['accommodations'] + registration.accommodation_request = post_vars.get('accommodations','') exam_info = course.testcenter_info registration.exam_series_code = exam_info.get('Exam_Series_Code') registration.eligibility_appointment_date_first = exam_info.get('First_Eligible_Appointment_Date') @@ -734,18 +740,8 @@ def _do_create_or_update_test_center_user(post_vars): except IntegrityError, ie: raise - return (user, testcenter_user, registration) +# return (user, testcenter_user, registration) -@ensure_csrf_cookie -def create_test_registration(request, post_override=None): - ''' - JSON call to create test registration. - Used by form in test_center_register.html, which is called from - into dashboard.html - ''' - js = {'success': False} - - post_vars = post_override if post_override else request.POST # Confirm we have a properly formed request # for a in ['first_name', 'last_name', 'address_1', 'city', 'country']: @@ -768,13 +764,12 @@ def create_test_registration(request, post_override=None): # return HttpResponse(json.dumps(js)) # Once the test_center_user information has been validated, create the entries: - ret = _do_create_or_update_test_center_user(post_vars) - if isinstance(ret,HttpResponse): # if there was an error then return that - return ret - - - (user, testcenter_user, testcenter_registration) = ret - +# ret = _do_create_or_update_test_center_user(post_vars) +# if isinstance(ret,HttpResponse): # if there was an error then return that +# return ret +# +# +# (user, testcenter_user, testcenter_registration) = ret # only do the following if there is accommodation text to send, # and a destination to which to send it: @@ -800,8 +795,9 @@ def create_test_registration(request, post_override=None): # TODO: enable appropriate stat # statsd.increment("common.student.account_created") - js = {'success': True} - return HttpResponse(json.dumps(js), mimetype="application/json") +# js = {'success': True} +# return HttpResponse(json.dumps(js), mimetype="application/json") + return HttpResponseRedirect(reverse('dashboard')) def get_random_post_override(): diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index d6a38e1bb4..fe175b11e4 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -10,18 +10,18 @@ <%namespace name='static' file='static_content.html'/> <%block name="title">Pearson VUE Test Center Proctoring - Sign Up - +<%doc> <%block name="js_extra"> + -<% -exam_info = course.testcenter_info -%>
    - - - - - <% - registrations = get_testcenter_registrations_for_user_and_course(user, course.id) - %> - - -
    -
    -

    - % if course.has_ended(): - Course Completed - ${course.end_date_text} - % elif course.has_started(): - Course Started - ${course.start_date_text} - % else: # hasn't started yet - Course Starts - ${course.start_date_text} - % endif -

    -

    ${get_course_about_section(course, 'university')}

    -

    ${course.number} ${course.title}

    -
    - - - % if exam_info is not None: -

    Exam Series Code: ${exam_info.get('Exam_Series_Code')}

    -

    First Eligible Appointment Date: ${exam_info.get('First_Eligible_Appointment_Date')}

    -

    Last Eligible Appointment Date: ${exam_info.get('Last_Eligible_Appointment_Date')}

    - % endif - - % if len(registrations) > 0: - <% - registration = registrations[0] - %> - -
    Already Registered
    -

    Here is the current state of your registration, for debugging purposes:

    - -
  • id: ${registration.id}
  • -
  • testcenter_user_id: ${registration.testcenter_user_id}
  • -
  • course_id: ${registration.course_id}
  • -
  • accommodation codes: ${registration.accommodation_code}
  • -
  • accommodation request: ${registration.accommodation_request}
  • -
  • created_at: ${registration.created_at}
  • -
  • updated_at: ${registration.updated_at}
  • -
  • user_updated_at: ${registration.user_updated_at}
  • -
  • upload_status: ${registration.upload_status}
  • -
  • upload_error_message: ${registration.upload_error_message}
  • -
    - - - - <% - regstatus = "registration pending acknowledgement by Pearson" - - if registration.upload_status == 'Accepted': - regstatus = "registration approved by Pearson" - elif registration.upload_status == 'Error': - regstatus = "registration rejected by Pearson: %s" % registration.upload_error_message - elif len(registration.accommodation_request) > 0 and registration.accommodation_code == '': - regstatus = "pending approval of accommodation request" - %> -

    Current status: ${regstatus}

    - -

    The demographic information provided below was used to register - for the exam listed above. Changes to this information - may be submitted below.

    -
    - - % else: -

    The demographic information must be provided below in order to register - for the exam listed above.

    - % endif -

    ${get_course_about_section(course, 'university')} ${course.number} ${course.title}

    - % if len(registrations) > 0: + % if registration:

    Your Pearson VUE Proctored Exam Registration

    % else:

    Register for a Pearson VUE Proctored Exam

    @@ -136,8 +55,9 @@ exam_info = course.testcenter_info
    - -
    + +

    Your registration data has been updated and saved.

    @@ -148,10 +68,12 @@ exam_info = course.testcenter_info
    % endif - + + % if form.errors and len(form.errors) > 0:

    We're Sorry, but there was an error with the information you provided below:

    + % endif
    @@ -160,7 +82,7 @@ exam_info = course.testcenter_info
    - % if len(registrations) > 0: + % if registration:

    Please complete the following form to update your demographic information used in your Pearson VUE Proctored Exam. Required fields are noted by bold text and an asterisk (*).

    @@ -181,15 +103,23 @@ exam_info = course.testcenter_info
    1. + + ${form['salutation']} +
    2. +
    3. + + ${form['first_name']} +
    4. + -
    5. + --> +
    6. @@ -281,7 +211,7 @@ exam_info = course.testcenter_info
      - % if len(registrations) > 0: + % if registration:

      Fields below this point are not part of the demographics, and are not editable currently.

      % else:

      Fields below this point are not part of the demographics, and cannot be changed once submitted.

      @@ -291,7 +221,7 @@ exam_info = course.testcenter_info
        - % if len(registrations) > 0: + % if registration:
      1. @@ -307,7 +237,7 @@ exam_info = course.testcenter_info
      - % if len(registrations) > 0: + % if registration: % else: @@ -317,12 +247,10 @@ exam_info = course.testcenter_info
    From 7b08114805a3410c4633817db38943469887e244 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:09:48 -0500 Subject: [PATCH 159/541] More restyling, fix buttons --- .../lib/xmodule/xmodule/css/combinedopenended/display.scss | 5 ++--- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 6adb31aa5d..7f52cd9623 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -30,7 +30,7 @@ section.combined-open-ended { { float:left; width: 53%; - padding-bottom: 20px; + padding-bottom: 50px; } .result-container @@ -78,8 +78,7 @@ div.result-container { } .feedback-on-feedback { - height: 150px; - width: 250px; + height: 100px; margin-right: 0px; } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 1dbe3fe9ea..0e285dce25 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -62,8 +62,10 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => if response.success @results_container.after(response.html).remove() - @results_container = $('.result-container') - @Collapsible.setCollapsibles(@results_container) + @results_container = $('div.result-container') + @submit_evaluation_button = $('.submit-evaluation-button') + @submit_evaluation_button.click @message_post + Collapsible.setCollapsibles(@results_container) else @errors_area.html(response.error) From af705ca1af28445abb85ca1a5268100654970ac7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:12:21 -0500 Subject: [PATCH 160/541] Remove html --- lms/templates/open_ended_evaluation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/open_ended_evaluation.html b/lms/templates/open_ended_evaluation.html index 71ce6d5056..da3f38b6a9 100644 --- a/lms/templates/open_ended_evaluation.html +++ b/lms/templates/open_ended_evaluation.html @@ -20,4 +20,4 @@
    -incorrect-icon.png \ No newline at end of file + \ No newline at end of file From bccafe6c3195403ce321efc4fb20e56d1cfc18e8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:23:54 -0500 Subject: [PATCH 161/541] Add in correctness display --- .../xmodule/combined_open_ended_module.py | 16 ++++++++++++---- common/lib/xmodule/xmodule/open_ended_module.py | 7 ------- common/lib/xmodule/xmodule/openendedchild.py | 11 +++++++++++ lms/templates/combined_open_ended.html | 1 - lms/templates/combined_open_ended_results.html | 2 +- lms/templates/combined_open_ended_status.html | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index aec330cb49..957b978407 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -218,11 +218,19 @@ class CombinedOpenEndedModule(XModule): last_post_assessment = task.latest_post_assessment(short_feedback=False) last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation + last_correctness = task.is_last_response_correct() max_score = task.max_score() state = task.state - last_response_dict={'response' : last_response, 'score' : last_score, - 'post_assessment' : last_post_assessment, - 'type' : task_type, 'max_score' : max_score, 'state' : state, 'human_state' : task.HUMAN_NAMES[state]} + last_response_dict={ + 'response' : last_response, + 'score' : last_score, + 'post_assessment' : last_post_assessment, + 'type' : task_type, + 'max_score' : max_score, + 'state' : state, + 'human_state' : task.HUMAN_NAMES[state], + 'correct' : last_correctness + } return last_response_dict @@ -252,7 +260,7 @@ class CombinedOpenEndedModule(XModule): task_number=int(get['task_number']) self.update_task_states() response_dict=self.get_last_response(task_number) - context = {'results' : response_dict['post_assessment']} + context = {'results' : response_dict['post_assessment'], 'task_number' : task_number+1} html = render_to_string('combined_open_ended_results.html', context) return {'html' : html, 'success' : True} diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ba1c68511f..9cdbdd54e2 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -397,13 +397,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' - def is_submission_correct(self, score): - correct=False - if(isinstance(score,(int, long, float, complex))): - score_ratio = int(score) / float(self.max_score()) - correct = (score_ratio >= 0.66) - return correct - def format_feedback_with_evaluation(self,feedback): context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} html= render_to_string('open_ended_evaluation.html', context) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 236bd03c4c..73bd8f3957 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -250,5 +250,16 @@ class OpenEndedChild(): def handle_ajax(self): pass + def is_submission_correct(self, score): + correct=False + if(isinstance(score,(int, long, float, complex))): + score_ratio = int(score) / float(self.max_score()) + correct = (score_ratio >= 0.66) + return correct + + def is_last_response_correct(self): + score=self.get_score() + return self.is_submission_correct(score) + diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 4c0aaa1042..71c22085e3 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -17,7 +17,6 @@
    -

    Results


    diff --git a/lms/templates/combined_open_ended_results.html b/lms/templates/combined_open_ended_results.html index 75c5596e4b..db86e95016 100644 --- a/lms/templates/combined_open_ended_results.html +++ b/lms/templates/combined_open_ended_results.html @@ -1,4 +1,4 @@
    -

    Results


    +

    Results from Step ${task_number}


    ${results | n}
    \ No newline at end of file diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 6109df2f26..9d51bd67fd 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,7 +8,7 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} - %if status['type']=="openended": + %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']: From 5150d6cdeec2664773acb7ac67d3f20461a120aa Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:28:02 -0500 Subject: [PATCH 162/541] Add in better status indicators --- common/lib/xmodule/xmodule/openendedchild.py | 3 ++- lms/templates/combined_open_ended_status.html | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 73bd8f3957..5fccdee4f6 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -259,7 +259,8 @@ class OpenEndedChild(): def is_last_response_correct(self): score=self.get_score() - return self.is_submission_correct(score) + correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' + return diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 9d51bd67fd..a653a84d13 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,6 +8,15 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} + % if state == 'initial': + Unanswered + % elif state in ['done', 'post_assessment'] and correct == 'correct': + Correct + % elif state in ['done', 'post_assessment'] and correct == 'incorrect': + Incorrect + % elif state == 'assessing': + Submitted for grading + % endif %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
    Show results from step ${status['task_number']} From 3bf532e199e62a942ccbb2f3a6164bbb2f142015 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:45:07 -0500 Subject: [PATCH 163/541] Check marks in each step --- .../css/combinedopenended/display.scss | 29 +++++++++++++++++++ common/lib/xmodule/xmodule/openendedchild.py | 4 +-- lms/templates/combined_open_ended_status.html | 17 ++++++----- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 7f52cd9623..be86757aee 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -67,6 +67,35 @@ section.combined-open-ended-status { font-size: 1em; padding-top: 10px; } + + span { + &.unanswered { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + position: relative; + width: 14px; + float: right; + } + + &.correct { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + position: relative; + width: 25px; + float: right; + } + + &.incorrect { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + position: relative; + float: right; + } + } } div.result-container { diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5fccdee4f6..5d69323e4a 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -258,9 +258,9 @@ class OpenEndedChild(): return correct def is_last_response_correct(self): - score=self.get_score() + score=self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' - return + return correctness diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index a653a84d13..34a5dd0d79 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,15 +8,16 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} - % if state == 'initial': - Unanswered - % elif state in ['done', 'post_assessment'] and correct == 'correct': - Correct - % elif state in ['done', 'post_assessment'] and correct == 'incorrect': - Incorrect - % elif state == 'assessing': - Submitted for grading + % if status['state'] == 'initial': + + % elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'correct': + + % elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'incorrect': + + % elif status['state'] == 'assessing': + % endif + %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
    Show results from step ${status['task_number']} From 9e14e22c91e28376424b2fb41ac642a82d3642f3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:53:36 -0500 Subject: [PATCH 164/541] Move prompt and rubric to combined open ended instead of defining them in each task --- .../xmodule/combined_open_ended_module.py | 12 +++++++++--- common/lib/xmodule/xmodule/open_ended_module.py | 17 ++++++----------- common/lib/xmodule/xmodule/openendedchild.py | 3 +++ .../xmodule/xmodule/self_assessment_module.py | 2 -- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 957b978407..4788c43382 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -98,6 +98,8 @@ class CombinedOpenEndedModule(XModule): self.static_data = { 'max_score' : self._max_score, 'max_attempts' : self.max_attempts, + 'prompt' : definition['prompt'], + 'rubric' : definition['rubric'] } self.task_xml=definition['task_xml'] @@ -371,16 +373,20 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): 'hintprompt': 'some-html' } """ - expected_children = ['task'] + expected_children = ['task', 'rubric', 'prompt'] for child in expected_children: if len(xml_object.xpath(child)) == 0 : raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) - def parse(k): + def parse_task(k): """Assumes that xml_object has child k""" return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] - return {'task_xml': parse('task')} + def parse(k): + """Assumes that xml_object has child k""" + return xml_object.xpath(k)[0] + + return {'task_xml': parse_task('task'), 'prompt' : parse('prompt'), 'rubric' : parse('rubric')} def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 9cdbdd54e2..b795db8228 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -40,8 +40,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): oeparam = definition['oeparam'] - prompt = definition['prompt'] - rubric = definition['rubric'] self.url = definition.get('url', None) self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE) @@ -53,12 +51,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if oeparam is None: raise ValueError("No oeparam found in problem xml.") - if prompt is None: + if self.prompt is None: raise ValueError("No prompt found in problem xml.") - if rubric is None: + if self.rubric is None: raise ValueError("No rubric found in problem xml.") - self._parse(oeparam, prompt, rubric, system) + self._parse(oeparam, self.prompt, self.rubric, system) if self.created=="True" and self.state == self.ASSESSING: self.created="False" @@ -530,7 +528,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): } """ - for child in ['openendedrubric', 'prompt', 'openendedparam']: + for child in ['openendedparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) @@ -538,10 +536,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'rubric': parse('openendedrubric'), - 'prompt': parse('prompt'), - 'oeparam': parse('openendedparam'), - } + return {'oeparam': parse('openendedparam'),} def definition_to_xml(self, resource_fs): @@ -553,7 +548,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in ['openendedrubric', 'prompt', 'openendedparam']: + for child in ['openendedparam']: add_child(child) return elt diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5d69323e4a..4a81703919 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -127,6 +127,9 @@ class OpenEndedChild(): self.attempts = instance_state.get('attempts', 0) self.max_attempts = static_data['max_attempts'] + self.prompt = static_data['prompt'] + self.rubric = static_data['rubric'] + # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = static_data['max_score'] diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 88c47f92ef..7050ff991b 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -33,8 +33,6 @@ log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): - self.rubric = definition['rubric'] - self.prompt = definition['prompt'] self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] From 3aa0daaa764176823b9d9e2811f73d7b4bfcdf05 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:00:30 -0500 Subject: [PATCH 165/541] Add in proper prompt and rubric parsing --- common/lib/xmodule/xmodule/open_ended_module.py | 1 + common/lib/xmodule/xmodule/self_assessment_module.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index b795db8228..3047c86888 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -76,6 +76,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): prompt_string = stringify_children(prompt) rubric_string = stringify_children(rubric) self.prompt=prompt_string + self.rubric=rubric_string grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 7050ff991b..db082a8b4f 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -35,6 +35,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] + self.prompt = stringify_children(prompt) + self.rubric = stringify_children(rubric) def get_html(self, system): #set context variables and render template @@ -270,7 +272,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): 'hintprompt': 'some-html' } """ - expected_children = ['rubric', 'prompt', 'submitmessage', 'hintprompt'] + expected_children = ['submitmessage', 'hintprompt'] for child in expected_children: if len(xml_object.xpath(child)) != 1: raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child)) @@ -279,9 +281,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return stringify_children(xml_object.xpath(k)[0]) - return {'rubric': parse('rubric'), - 'prompt': parse('prompt'), - 'submitmessage': parse('submitmessage'), + return {'submitmessage': parse('submitmessage'), 'hintprompt': parse('hintprompt'), } @@ -294,7 +294,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in ['rubric', 'prompt', 'submitmessage', 'hintprompt']: + for child in ['submitmessage', 'hintprompt']: add_child(child) return elt From dcb33f1d5f2d2189968e813764ba4f97acca09ee Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:04:09 -0500 Subject: [PATCH 166/541] Fix prompt and rubric passing --- common/lib/xmodule/xmodule/self_assessment_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index db082a8b4f..52701a8cf1 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -35,8 +35,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] - self.prompt = stringify_children(prompt) - self.rubric = stringify_children(rubric) + self.prompt = stringify_children(self.prompt) + self.rubric = stringify_children(self.rubric) def get_html(self, system): #set context variables and render template From b6a49f33ad0d678265bff5a826b6e2b678711e31 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:31:33 -0500 Subject: [PATCH 167/541] parse out min and max score to advance --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 11 +++++++---- common/lib/xmodule/xmodule/open_ended_module.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4788c43382..0e9874ec4f 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -82,7 +82,6 @@ class CombinedOpenEndedModule(XModule): # element. # Scores are on scale from 0 to max_score system.set('location', location) - log.debug(system.location) self.current_task_number = instance_state.get('current_task_number', 0) self.task_states= instance_state.get('task_states', []) @@ -147,7 +146,13 @@ class CombinedOpenEndedModule(XModule): children=self.child_modules() self.current_task_descriptor=children['descriptors'][current_task_type](self.system) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) + etree_xml=etree.fromstring(self.current_task_xml) + min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) + max_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',self._max_score)) + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) @@ -165,8 +170,6 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.overwrite_state(current_task_state) self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) - log.debug(self.current_task.get_instance_state()) - log.debug(self.get_instance_state()) return True def get_context(self): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3047c86888..0e16156f1a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -528,7 +528,6 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): 'oeparam': 'some-html' } """ - for child in ['openendedparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) From 1ae94ce6366f2dc00a7159a9e06d4578c262bf8d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:45:21 -0500 Subject: [PATCH 168/541] Hopefully allow for submission to be reset early --- .../xmodule/combined_open_ended_module.py | 33 +++++++++++++++---- .../js/src/combinedopenended/display.coffee | 11 ++++--- common/lib/xmodule/xmodule/openendedchild.py | 2 +- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 0e9874ec4f..d31da49978 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -147,10 +147,11 @@ class CombinedOpenEndedModule(XModule): self.current_task_descriptor=children['descriptors'][current_task_type](self.system) etree_xml=etree.fromstring(self.current_task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',self._max_score)) + if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) + allow_reset=self.check_allow_reset() + if allow_reset: + return False self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: @@ -172,6 +173,17 @@ class CombinedOpenEndedModule(XModule): return True + def check_allow_reset(self): + allow_reset=False + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + current_response_data=self.get_last_response(self.current_task_number) + + if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt'] if response.success @child_state = 'done' - @allow_reset = response.allow_reset @rebind() else @errors_area.html(response.error) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 4a81703919..304271c620 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -200,7 +200,7 @@ class OpenEndedChild(): def _allow_reset(self): """Can the module be reset?""" - return self.state == self.DONE and self.attempts < self.max_attempts + return (self.state == self.DONE and self.attempts < self.max_attempts) def max_score(self): """ From b13f94798fd3ff5fe37c5c509a9b94c78de671f1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 17:02:56 -0500 Subject: [PATCH 169/541] Add in allow reset action --- .../xmodule/combined_open_ended_module.py | 39 ++++++++++++------- .../js/src/combinedopenended/display.coffee | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index d31da49978..f9c610d51a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -88,6 +88,7 @@ class CombinedOpenEndedModule(XModule): self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) + self.allow_reset = instance_state.get('ready_to_reset', False) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) # Used for progress / grading. Currently get credit just for @@ -149,8 +150,8 @@ class CombinedOpenEndedModule(XModule): etree_xml=etree.fromstring(self.current_task_xml) if self.current_task_number>0: - allow_reset=self.check_allow_reset() - if allow_reset: + self.allow_reset=self.check_allow_reset() + if self.allow_reset: return False self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) @@ -174,15 +175,15 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): - allow_reset=False if self.current_task_number>0: last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_last_response(self.current_task_number) + current_response_data=self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt']last_response_data['score'] or current_response_data['max_score_to_attempt']=(len(self.task_xml)): self.state=self.DONE @@ -334,6 +343,7 @@ class CombinedOpenEndedModule(XModule): 'error': 'Too many attempts.' } self.state=self.INITIAL + self.allow_reset=False for i in xrange(0,len(self.task_xml)): self.current_task_number=i self.setup_next_task(reset=True) @@ -354,6 +364,7 @@ class CombinedOpenEndedModule(XModule): 'state': self.state, 'task_states': self.task_states, 'attempts': self.attempts, + 'ready_to_reset' : self.allow_reset, } return json.dumps(state) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 0d420a8514..3929ebe78a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -79,7 +79,7 @@ class @CombinedOpenEnded if @child_type=="openended" @skip_button.hide() - if @allow_reset + if @allow_reset=="True" @reset_button.show() @submit_button.hide() @answer_area.attr("disabled", true) From adcbfbb6035e748cf4b0cecaaf2e4fee3d563113 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:00:46 -0500 Subject: [PATCH 170/541] Fix reset --- .../xmodule/combined_open_ended_module.py | 55 ++++++++++--------- .../js/src/combinedopenended/display.coffee | 3 +- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f9c610d51a..3c756f6834 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -142,6 +142,12 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.task_states[self.current_task_number] self.current_task_xml=self.task_xml[self.current_task_number] + + if self.current_task_number>0: + self.allow_reset=self.check_allow_reset() + if self.allow_reset: + self.current_task_number=self.current_task_number-1 + current_task_type=self.get_tag_name(self.current_task_xml) children=self.child_modules() @@ -149,11 +155,6 @@ class CombinedOpenEndedModule(XModule): self.current_task_descriptor=children['descriptors'][current_task_type](self.system) etree_xml=etree.fromstring(self.current_task_xml) - if self.current_task_number>0: - self.allow_reset=self.check_allow_reset() - if self.allow_reset: - return False - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) @@ -175,13 +176,14 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): - if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_current_attributes(self.current_task_number) + if not self.allow_reset: + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + current_response_data=self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt']last_response_data['score'] or current_response_data['max_score_to_attempt']=(len(self.task_xml)): - self.state=self.DONE - self.current_task_number=len(self.task_xml)-1 - else: - self.state=self.INITIAL - changed=True - self.setup_next_task() + if not self.allow_reset: + self.task_states[self.current_task_number] = self.current_task.get_instance_state() + current_task_state=json.loads(self.task_states[self.current_task_number]) + if current_task_state['state']==self.DONE: + self.current_task_number+=1 + if self.current_task_number>=(len(self.task_xml)): + self.state=self.DONE + self.current_task_number=len(self.task_xml)-1 + else: + self.state=self.INITIAL + changed=True + self.setup_next_task() return changed def update_task_states_ajax(self,return_html): @@ -335,7 +336,8 @@ class CombinedOpenEndedModule(XModule): (error only present if not success) """ if self.state != self.DONE: - return self.out_of_sync_error(get) + if not self.allow_reset: + return self.out_of_sync_error(get) if self.attempts > self.max_attempts: return { @@ -350,6 +352,7 @@ class CombinedOpenEndedModule(XModule): self.current_task.reset(self.system) self.task_states[self.current_task_number]=self.current_task.get_instance_state() self.current_task_number=0 + self.allow_reset=False self.setup_next_task() return {'success': True, 'html' : self.get_html_nonsystem()} diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 3929ebe78a..a7e01e8a9b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -187,7 +187,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() - if @child_state == 'done' + if @child_state == 'done' or @allow_reset=="True" $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success @answer_area.val('') @@ -196,6 +196,7 @@ class @CombinedOpenEnded @message_wrapper.html('') @child_state = 'initial' @combined_open_ended.after(response.html).remove() + @allow_reset="False" @reinitialize(@element) @rebind() @reset_button.hide() From bc97a507e047cb8fca8626b303e1507fa57ae930 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:05:01 -0500 Subject: [PATCH 171/541] Next step logic --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 3c756f6834..ee36690b1c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -326,7 +326,7 @@ class CombinedOpenEndedModule(XModule): def next_problem(self, get): self.update_task_states() - return {'success' : True, 'html' : self.get_html_nonsystem()} + return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} def reset(self, get): """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a7e01e8a9b..682ba983bd 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -218,7 +218,10 @@ class @CombinedOpenEnded @reinitialize(@element) @rebind() @next_problem_button.hide() - @gentle_alert "Moved to next step." + if response.allow_reset=="False" + @gentle_alert "Moved to next step." + else + @gentle_alert "Your score did not meet the criteria to move to the next step." else @errors_area.html(response.error) else From 38a81b461f413d8a7e4cc00a7793d0de8d8b6894 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:33:41 -0500 Subject: [PATCH 172/541] Remove open ended grading stuff, fix JS variable --- common/lib/capa/capa/inputtypes.py | 48 -- common/lib/capa/capa/responsetypes.py | 433 +----------------- .../xmodule/js/src/capa/display.coffee | 30 -- .../js/src/combinedopenended/display.coffee | 2 +- .../lib/xmodule/xmodule/open_ended_module.py | 2 +- 5 files changed, 3 insertions(+), 512 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index e3eb47acc5..1d3646fefc 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -735,51 +735,3 @@ class ChemicalEquationInput(InputTypeBase): registry.register(ChemicalEquationInput) #----------------------------------------------------------------------------- - -class OpenEndedInput(InputTypeBase): - """ - A text area input for code--uses codemirror, does syntax highlighting, special tab handling, - etc. - """ - - template = "openendedinput.html" - tags = ['openendedinput'] - - # pulled out for testing - submitted_msg = ("Feedback not yet available. Reload to check again. " - "Once the problem is graded, this message will be " - "replaced with the grader's feedback.") - - @classmethod - def get_attributes(cls): - """ - Convert options to a convenient format. - """ - return [Attribute('rows', '30'), - Attribute('cols', '80'), - Attribute('hidden', ''), - ] - - def setup(self): - """ - Implement special logic: handle queueing state, and default input. - """ - # if no student input yet, then use the default input given by the problem - if not self.value: - self.value = self.xml.text - - # Check if problem has been queued - self.queue_len = 0 - # Flag indicating that the problem has been queued, 'msg' is length of queue - if self.status == 'incomplete': - self.status = 'queued' - self.queue_len = self.msg - self.msg = self.submitted_msg - - def _extra_context(self): - """Defined queue_len, add it """ - return {'queue_len': self.queue_len,} - -registry.register(OpenEndedInput) - -#----------------------------------------------------------------------------- diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 1bc34b70a3..3d97cb0bea 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1815,436 +1815,6 @@ class ImageResponse(LoncapaResponse): return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]), dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) #----------------------------------------------------------------------------- - -class OpenEndedResponse(LoncapaResponse): - """ - Grade student open ended responses using an external grading system, - accessed through the xqueue system. - - Expects 'xqueue' dict in ModuleSystem with the following keys that are - needed by OpenEndedResponse: - - system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL - where results are posted (string), - } - - External requests are only submitted for student submission grading - (i.e. and not for getting reference answers) - - By default, uses the OpenEndedResponse.DEFAULT_QUEUE queue. - """ - - DEFAULT_QUEUE = 'open-ended' - DEFAULT_MESSAGE_QUEUE = 'open-ended-message' - response_tag = 'openendedresponse' - allowed_inputfields = ['openendedinput'] - max_inputfields = 1 - - def setup_response(self): - ''' - Configure OpenEndedResponse from XML. - ''' - xml = self.xml - self.url = xml.get('url', None) - self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE) - self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE) - - # The openendedparam tag encapsulates all grader settings - oeparam = self.xml.find('openendedparam') - prompt = self.xml.find('prompt') - rubric = self.xml.find('openendedrubric') - - #This is needed to attach feedback to specific responses later - self.submission_id=None - self.grader_id=None - - if oeparam is None: - raise ValueError("No oeparam found in problem xml.") - if prompt is None: - raise ValueError("No prompt found in problem xml.") - if rubric is None: - raise ValueError("No rubric found in problem xml.") - - self._parse(oeparam, prompt, rubric) - - @staticmethod - def stringify_children(node): - """ - Modify code from stringify_children in xmodule. Didn't import directly - in order to avoid capa depending on xmodule (seems to be avoided in - code) - """ - parts=[node.text if node.text is not None else ''] - for p in node.getchildren(): - parts.append(etree.tostring(p, with_tail=True, encoding='unicode')) - - return ' '.join(parts) - - def _parse(self, oeparam, prompt, rubric): - ''' - Parse OpenEndedResponse XML: - self.initial_display - self.payload - dict containing keys -- - 'grader' : path to grader settings file, 'problem_id' : id of the problem - - self.answer - What to display when show answer is clicked - ''' - # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload - prompt_string = self.stringify_children(prompt) - rubric_string = self.stringify_children(rubric) - - grader_payload = oeparam.find('grader_payload') - grader_payload = grader_payload.text if grader_payload is not None else '' - - #Update grader payload with student id. If grader payload not json, error. - try: - parsed_grader_payload = json.loads(grader_payload) - # NOTE: self.system.location is valid because the capa_module - # __init__ adds it (easiest way to get problem location into - # response types) - except TypeError, ValueError: - log.exception("Grader payload %r is not a json object!", grader_payload) - - self.initial_display = find_with_default(oeparam, 'initial_display', '') - self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') - - parsed_grader_payload.update({ - 'location' : self.system.location, - 'course_id' : self.system.course_id, - 'prompt' : prompt_string, - 'rubric' : rubric_string, - 'initial_display' : self.initial_display, - 'answer' : self.answer, - }) - updated_grader_payload = json.dumps(parsed_grader_payload) - - self.payload = {'grader_payload': updated_grader_payload} - - try: - self.max_score = int(find_with_default(oeparam, 'max_score', 1)) - except ValueError: - self.max_score = 1 - - def handle_message_post(self,event_info): - """ - Handles a student message post (a reaction to the grade they received from an open ended grader type) - Returns a boolean success/fail and an error message - """ - survey_responses=event_info['survey_responses'] - for tag in ['feedback', 'submission_id', 'grader_id', 'score']: - if tag not in survey_responses: - return False, "Could not find needed tag {0}".format(tag) - try: - submission_id=int(survey_responses['submission_id']) - grader_id = int(survey_responses['grader_id']) - feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) - score = int(survey_responses['score']) - except: - error_message=("Could not parse submission id, grader id, " - "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) - log.exception(error_message) - return False, "There was an error saving your feedback. Please contact course staff." - - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - anonymous_student_id = self.system.anonymous_student_id - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.answer_id) - - xheader = xqueue_interface.make_xheader( - lms_callback_url=self.system.xqueue['callback_url'], - lms_key=queuekey, - queue_name=self.message_queue_name - ) - - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime, - } - contents= { - 'feedback' : feedback, - 'submission_id' : submission_id, - 'grader_id' : grader_id, - 'score': score, - 'student_info' : json.dumps(student_info), - } - - (error, msg) = qinterface.send_to_queue(header=xheader, - body=json.dumps(contents)) - - #Convert error to a success value - success=True - if error: - success=False - - return success, "Successfully submitted your feedback." - - def get_score(self, student_answers): - - try: - submission = student_answers[self.answer_id] - except KeyError: - msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}' - .format(self.answer_id, student_answers)) - log.exception(msg) - raise LoncapaProblemError(msg) - - # Prepare xqueue request - #------------------------------------------------------------ - - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - - anonymous_student_id = self.system.anonymous_student_id - - # Generate header - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.answer_id) - - xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], - lms_key=queuekey, - queue_name=self.queue_name) - - self.context.update({'submission': submission}) - - contents = self.payload.copy() - - # Metadata related to the student submission revealed to the external grader - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime, - } - - #Update contents with student response and student info - contents.update({ - 'student_info': json.dumps(student_info), - 'student_response': submission, - 'max_score' : self.max_score, - }) - - # Submit request. When successful, 'msg' is the prior length of the queue - (error, msg) = qinterface.send_to_queue(header=xheader, - body=json.dumps(contents)) - - # State associated with the queueing request - queuestate = {'key': queuekey, - 'time': qtime,} - - cmap = CorrectMap() - if error: - cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: {0}.)' - ' Please try again later.'.format(msg)) - else: - # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that - # the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down - # through inputtypes.textbox and .filesubmission to inform the - # browser that the submission is queued (and it could e.g. poll) - cmap.set(self.answer_id, queuestate=queuestate, - correctness='incomplete', msg=msg) - - return cmap - - def update_score(self, score_msg, oldcmap, queuekey): - log.debug(score_msg) - score_msg = self._parse_score_msg(score_msg) - if not score_msg.valid: - oldcmap.set(self.answer_id, - msg = 'Invalid grader reply. Please contact the course staff.') - return oldcmap - - correctness = 'correct' if score_msg.correct else 'incorrect' - - # TODO: Find out how this is used elsewhere, if any - self.context['correct'] = correctness - - # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey - # does not match, we keep waiting for the score_msg whose key actually matches - if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points - points = score_msg.points - if points < 0: - points = 0 - - # Queuestate is consumed, so reset it to None - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, - msg = score_msg.msg.replace(' ', ' '), queuestate=None) - else: - log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format( - queuekey, self.answer_id)) - - return oldcmap - - def get_answers(self): - anshtml = '
    {0}
    '.format(self.answer) - return {self.answer_id: anshtml} - - def get_initial_display(self): - return {self.answer_id: self.initial_display} - - def _convert_longform_feedback_to_html(self, response_items): - """ - Take in a dictionary, and return html strings for display to student. - Input: - response_items: Dictionary with keys success, feedback. - if success is True, feedback should be a dictionary, with keys for - types of feedback, and the corresponding feedback values. - if success is False, feedback is actually an error string. - - NOTE: this will need to change when we integrate peer grading, because - that will have more complex feedback. - - Output: - String -- html that can be displayed to the student. - """ - - # We want to display available feedback in a particular order. - # This dictionary specifies which goes first--lower first. - priorities = {# These go at the start of the feedback - 'spelling': 0, - 'grammar': 1, - # needs to be after all the other feedback - 'markup_text': 3} - - default_priority = 2 - - def get_priority(elt): - """ - Args: - elt: a tuple of feedback-type, feedback - Returns: - the priority for this feedback type - """ - return priorities.get(elt[0], default_priority) - - def encode_values(feedback_type,value): - feedback_type=str(feedback_type).encode('ascii', 'ignore') - if not isinstance(value,basestring): - value=str(value) - value=value.encode('ascii', 'ignore') - return feedback_type,value - - def format_feedback(feedback_type, value): - feedback_type,value=encode_values(feedback_type,value) - feedback= """ -
    - {value} -
    - """.format(feedback_type=feedback_type, value=value) - return feedback - - def format_feedback_hidden(feedback_type , value): - feedback_type,value=encode_values(feedback_type,value) - feedback = """ - - """.format(feedback_type=feedback_type, value=value) - return feedback - - # TODO (vshnayder): design and document the details of this format so - # that we can do proper escaping here (e.g. are the graders allowed to - # include HTML?) - - for tag in ['success', 'feedback', 'submission_id', 'grader_id']: - if tag not in response_items: - return format_feedback('errors', 'Error getting feedback') - - feedback_items = response_items['feedback'] - try: - feedback = json.loads(feedback_items) - except (TypeError, ValueError): - log.exception("feedback_items have invalid json %r", feedback_items) - return format_feedback('errors', 'Could not parse feedback') - - if response_items['success']: - if len(feedback) == 0: - return format_feedback('errors', 'No feedback available') - - feedback_lst = sorted(feedback.items(), key=get_priority) - feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst) - else: - feedback_list_part1 = format_feedback('errors', response_items['feedback']) - - feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value) - for feedback_type,value in response_items.items() - if feedback_type in ['submission_id', 'grader_id']])) - - return u"\n".join([feedback_list_part1,feedback_list_part2]) - - def _format_feedback(self, response_items): - """ - Input: - Dictionary called feedback. Must contain keys seen below. - Output: - Return error message or feedback template - """ - - feedback = self._convert_longform_feedback_to_html(response_items) - - if not response_items['success']: - return self.system.render_template("open_ended_error.html", - {'errors' : feedback}) - - feedback_template = self.system.render_template("open_ended_feedback.html", { - 'grader_type': response_items['grader_type'], - 'score': "{0} / {1}".format(response_items['score'], self.max_score), - 'feedback': feedback, - }) - - return feedback_template - - - def _parse_score_msg(self, score_msg): - """ - Grader reply is a JSON-dump of the following dict - { 'correct': True/False, - 'score': Numeric value (floating point is okay) to assign to answer - 'msg': grader_msg - 'feedback' : feedback from grader - } - - Returns (valid_score_msg, correct, score, msg): - valid_score_msg: Flag indicating valid score_msg format (Boolean) - correct: Correctness of submission (Boolean) - score: Points to be assigned (numeric, can be float) - """ - fail = ScoreMessage(valid=False, correct=False, points=0, msg='') - try: - score_result = json.loads(score_msg) - except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) - return fail - - if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) - return fail - - for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: - if tag not in score_result: - log.error("External grader message is missing required tag: {0}" - .format(tag)) - return fail - - feedback = self._format_feedback(score_result) - self.submission_id=score_result['submission_id'] - self.grader_id=score_result['grader_id'] - - # HACK: for now, just assume it's correct if you got more than 2/3. - # Also assumes that score_result['score'] is an integer. - score_ratio = int(score_result['score']) / float(self.max_score) - correct = (score_ratio >= 0.66) - - #Currently ignore msg and only return feedback (which takes the place of msg) - return ScoreMessage(valid=True, correct=correct, - points=score_result['score'], msg=feedback) - -#----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration @@ -2261,5 +1831,4 @@ __all__ = [CodeResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, - JavascriptResponse, - OpenEndedResponse] + JavascriptResponse] diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index ba746fecb8..1c0ace9e59 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -25,7 +25,6 @@ class @Problem @$('section.action input.reset').click @reset @$('section.action input.show').click @show @$('section.action input.save').click @save - @$('section.evaluation input.submit-message').click @message_post # Collapsibles Collapsible.setCollapsibles(@el) @@ -198,35 +197,6 @@ class @Problem else @gentle_alert response.success - message_post: => - Logger.log 'message_post', @answers - - fd = new FormData() - feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value - submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML - grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML - score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() - fd.append('feedback', feedback) - fd.append('submission_id', submission_id) - fd.append('grader_id', grader_id) - if(!score) - @gentle_alert "You need to pick a rating before you can submit." - return - else - fd.append('score', score) - - - settings = - type: "POST" - data: fd - processData: false - contentType: false - success: (response) => - @gentle_alert response.message - @$('section.evaluation').slideToggle() - - $.ajaxWithPrefix("#{@url}/message_post", settings) - reset: => Logger.log 'problem_reset', @answers $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 682ba983bd..8a5ef42270 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -218,7 +218,7 @@ class @CombinedOpenEnded @reinitialize(@element) @rebind() @next_problem_button.hide() - if response.allow_reset=="False" + if !response.allow_reset @gentle_alert "Moved to next step." else @gentle_alert "Your score did not meet the criteria to move to the next step." diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0e16156f1a..024422773d 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -245,7 +245,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): that will have more complex feedback. Output: - String -- html that can be displayed to the student. + String -- html that can be displayincorrect-icon.pnged to the student. """ # We want to display available feedback in a particular order. From 2f841c8a334d329ea5d281668d714abd21c4d8d1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:55:28 -0500 Subject: [PATCH 173/541] Document combined open ended module --- .../xmodule/combined_open_ended_module.py | 144 ++++++++++++++++-- 1 file changed, 128 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index ee36690b1c..a639d6997a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -36,6 +36,10 @@ MAX_ATTEMPTS = 10000 MAX_SCORE = 1 class CombinedOpenEndedModule(XModule): + """ + This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). + It transitions between problems, and support arbitrary ordering. + """ STATE_VERSION = 1 # states @@ -59,16 +63,37 @@ class CombinedOpenEndedModule(XModule): instance_state, shared_state, **kwargs) """ - Definition file should have multiple task blocks: + Definition file should have one or many task blocks, a rubric block, and a prompt block: Sample file: - - - + + + Blah blah rubric. + + + Some prompt. + + + + What hint about this problem would you give to someone? + + + Save Succcesful. Thanks for participating! + + + + + Enter essay here. + This is the answer. + {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + + + + """ # Load instance state @@ -77,17 +102,19 @@ class CombinedOpenEndedModule(XModule): else: instance_state = {} - # History is a list of tuples of (answer, score, hint), where hint may be - # None for any element, and score and hint can be None for the last (current) - # element. - # Scores are on scale from 0 to max_score + #We need to set the location here so the child modules can use it system.set('location', location) - self.current_task_number = instance_state.get('current_task_number', 0) - self.task_states= instance_state.get('task_states', []) + #Tells the system which xml definition to load + self.current_task_number = instance_state.get('current_task_number', 0) + #This loads the states of the individual children + self.task_states= instance_state.get('task_states', []) + #Overall state of the combined open ended module self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) + + #Allow reset is true if student has failed the criteria to move to the next child task self.allow_reset = instance_state.get('ready_to_reset', False) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) @@ -95,6 +122,7 @@ class CombinedOpenEndedModule(XModule): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) + #Static data is passed to the child modules to render self.static_data = { 'max_score' : self._max_score, 'max_attempts' : self.max_attempts, @@ -106,10 +134,21 @@ class CombinedOpenEndedModule(XModule): self.setup_next_task() def get_tag_name(self, xml): + """ + Gets the tag name of a given xml block. + Input: XML string + Output: The name of the root tag + """ tag=etree.fromstring(xml).tag return tag def overwrite_state(self, current_task_state): + """ + Overwrites an instance state and sets the latest response to the current response. This is used + to ensure that the student response is carried over from the first child to the rest. + Input: Task state json string + Output: Task state json string + """ last_response_data=self.get_last_response(self.current_task_number-1) last_response = last_response_data['response'] @@ -122,6 +161,12 @@ class CombinedOpenEndedModule(XModule): return current_task_state def child_modules(self): + """ + Returns the functions associated with the child modules in a dictionary. This makes writing functions + simpler (saves code duplication) + Input: None + Output: A dictionary of dictionaries containing the descriptor functions and module functions + """ child_modules={ 'openended' : open_ended_module.OpenEndedModule, 'selfassessment' : self_assessment_module.SelfAssessmentModule, @@ -137,6 +182,12 @@ class CombinedOpenEndedModule(XModule): return children def setup_next_task(self, reset=False): + """ + Sets up the next task for the module. Creates an instance state if none exists, carries over the answer + from the last instance state to the next if needed. + Input: A boolean indicating whether or not the reset function is calling. + Output: Boolean True (not useful right now) + """ current_task_state=None if len(self.task_states)>self.current_task_number: current_task_state=self.task_states[self.current_task_number] @@ -176,6 +227,12 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): + """ + Checks to see if the student has passed the criteria to move to the next module. If not, sets + allow_reset to true and halts the student progress through the tasks. + Input: None + Output: the allow_reset attribute of the current module. + """ if not self.allow_reset: if self.current_task_number>0: last_response_data=self.get_last_response(self.current_task_number-1) @@ -188,6 +245,11 @@ class CombinedOpenEndedModule(XModule): return self.allow_reset def get_context(self): + """ + Generates a context dictionary that is used to render html. + Input: None + Output: A dictionary that can be rendered into the combined open ended template. + """ task_html=self.get_html_base() #set context variables and render template @@ -200,27 +262,47 @@ class CombinedOpenEndedModule(XModule): 'task_number' : self.current_task_number+1, 'status' : self.get_status(), } - log.debug(context) return context def get_html(self): + """ + Gets HTML for rendering. + Input: None + Output: rendered html + """ context=self.get_context() html = self.system.render_template('combined_open_ended.html', context) return html def get_html_nonsystem(self): + """ + Gets HTML for rendering via AJAX. Does not use system, because system contains some additional + html, which is not appropriate for returning via ajax calls. + Input: None + Output: HTML rendered directly via Mako + """ context=self.get_context() html = render_to_string('combined_open_ended.html', context) return html def get_html_base(self): + """ + Gets the HTML associated with the current child task + Input: None + Output: Child task HTML + """ self.update_task_states() html = self.current_task.get_html(self.system) return_html = rewrite_links(html, self.rewrite_content_links) return return_html def get_current_attributes(self, task_number): + """ + Gets the min and max score to attempt attributes of the specified task. + Input: The number of the task. + Output: The minimum and maximum scores needed to move on to the specified task. + """ task_xml=self.task_xml[task_number] etree_xml=etree.fromstring(task_xml) min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) @@ -228,6 +310,11 @@ class CombinedOpenEndedModule(XModule): return {'min_score_to_attempt' : min_score_to_attempt, 'max_score_to_attempt' : max_score_to_attempt} def get_last_response(self, task_number): + """ + Returns data associated with the specified task number, such as the last response, score, etc. + Input: The number of the task. + Output: A dictionary that contains information about the specified task. + """ last_response="" task_state = self.task_states[task_number] task_xml=self.task_xml[task_number] @@ -270,6 +357,11 @@ class CombinedOpenEndedModule(XModule): return last_response_dict def update_task_states(self): + """ + Updates the task state of the combined open ended module with the task state of the current child module. + Input: None + Output: boolean indicating whether or not the task state changed. + """ changed=False if not self.allow_reset: self.task_states[self.current_task_number] = self.current_task.get_instance_state() @@ -286,6 +378,11 @@ class CombinedOpenEndedModule(XModule): return changed def update_task_states_ajax(self,return_html): + """ + Runs the update task states function for ajax calls. Currently the same as update_task_states + Input: The html returned by the handle_ajax function of the child + Output: New html that should be rendered + """ changed=self.update_task_states() if changed: #return_html=self.get_html() @@ -293,6 +390,11 @@ class CombinedOpenEndedModule(XModule): return return_html def get_results(self, get): + """ + Gets the results of a given grader via ajax. + Input: AJAX get dictionary + Output: Dictionary to be rendered via ajax that contains the result html. + """ task_number=int(get['task_number']) self.update_task_states() response_dict=self.get_last_response(task_number) @@ -325,15 +427,19 @@ class CombinedOpenEndedModule(XModule): return json.dumps(d,cls=ComplexEncoder) def next_problem(self, get): + """ + Called via ajax to advance to the next problem. + Input: AJAX get request. + Output: Dictionary to be rendered + """ self.update_task_states() return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} def reset(self, get): """ - If resetting is allowed, reset the state. - - Returns {'success': bool, 'error': msg} - (error only present if not success) + If resetting is allowed, reset the state of the combined open ended module. + Input: AJAX get dictionary + Output: AJAX dictionary to tbe rendered """ if self.state != self.DONE: if not self.allow_reset: @@ -358,7 +464,9 @@ class CombinedOpenEndedModule(XModule): def get_instance_state(self): """ - Get the current score and state + Returns the current instance state. The module can be recreated from the instance state. + Input: None + Output: A dictionary containing the instance state. """ state = { @@ -373,6 +481,10 @@ class CombinedOpenEndedModule(XModule): return json.dumps(state) def get_status(self): + """ + Input: + Output: + """ status=[] for i in xrange(0,self.current_task_number+1): task_data = self.get_last_response(i) From 134c3f3db08250a3019591b287c868ee58ea1ec3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:57:37 -0500 Subject: [PATCH 174/541] Document open ended descriptor --- .../xmodule/xmodule/combined_open_ended_module.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a639d6997a..4bc0c1fc85 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -482,8 +482,9 @@ class CombinedOpenEndedModule(XModule): def get_status(self): """ - Input: - Output: + Gets the status panel to be displayed at the top right. + Input: None + Output: The status html to be rendered """ status=[] for i in xrange(0,self.current_task_number+1): @@ -497,7 +498,7 @@ class CombinedOpenEndedModule(XModule): class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ - Module for adding self assessment questions to courses + Module for adding combined open ended questions """ mako_template = "widgets/html-edit.html" module_class = CombinedOpenEndedModule @@ -513,14 +514,13 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): """ - Pull out the rubric, prompt, and submitmessage into a dictionary. + Pull out the individual tasks, the rubric, and the prompt, and parse Returns: { 'rubric': 'some-html', 'prompt': 'some-html', - 'submitmessage': 'some-html' - 'hintprompt': 'some-html' + 'task_xml': dictionary of xml strings, } """ expected_children = ['task', 'rubric', 'prompt'] From 5303086a1044fef7ce5da357335a9387a99b5c31 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:07:34 -0500 Subject: [PATCH 175/541] Start commenting open ended module --- .../lib/xmodule/xmodule/open_ended_module.py | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 024422773d..e4008309cd 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -37,8 +37,18 @@ from datetime import datetime log = logging.getLogger("mitx.courseware") class OpenEndedModule(openendedchild.OpenEndedChild): - + """ + The open ended module supports all external open ended grader problems. + """ def setup_response(self, system, location, definition, descriptor): + """ + Sets up the response type. + @param system: Modulesystem object + @param location: The location of the problem + @param definition: The xml definition of the problem + @param descriptor: The OpenEndedDescriptor associated with this + @return: None + """ oeparam = definition['oeparam'] self.url = definition.get('url', None) @@ -106,6 +116,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.payload = {'grader_payload': updated_grader_payload} def skip_post_assessment(self, get, system): + """ + Ajax function that allows one to skip the post assessment phase + @param get: AJAX dictionary + @param system: ModuleSystem + @return: Success indicator + """ self.state=self.DONE return {'success' : True} @@ -172,6 +188,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'success' : success, 'msg' : "Successfully submitted your feedback."} def send_to_grader(self, submission, system): + """ + Send a given submission to the grader, via the xqueue + @param submission: The student submission to send to the grader + @param system: Modulesystem + @return: Boolean true (not useful right now) + """ # Prepare xqueue request #------------------------------------------------------------ @@ -214,6 +236,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return True def _update_score(self, score_msg, queuekey, system): + """ + Called by xqueue to update the score + @param score_msg: The message from xqueue + @param queuekey: The key sent by xqueue + @param system: Modulesystem + @return: Boolean True (not useful currently) + """ new_score_msg = self._parse_score_msg(score_msg) if not new_score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' @@ -226,10 +255,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def get_answers(self): + """ + Gets and shows the answer for this problem. + @return: Answer html + """ anshtml = '
    {0}
    '.format(self.answer) return {self.answer_id: anshtml} def get_initial_display(self): + """ + Gets and shows the initial display for the input box. + @return: Initial display html + """ return {self.answer_id: self.initial_display} def _convert_longform_feedback_to_html(self, response_items): @@ -385,7 +422,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} def latest_post_assessment(self, short_feedback=False): - """None if not available""" + """ + Gets the latest feedback, parses, and returns + @param short_feedback: If the long feedback is wanted or not + @return: Returns formatted feedback + """ if not self.history: return "" @@ -397,6 +438,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return short_feedback if feedback_dict['valid'] else '' def format_feedback_with_evaluation(self,feedback): + """ + Renders a given html feedback into an evaluation template + @param feedback: HTML feedback + @return: Rendered html + """ context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} html= render_to_string('open_ended_evaluation.html', context) return html @@ -432,10 +478,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return json.dumps(d, cls=ComplexEncoder) def check_for_score(self, get, system): + """ + Checks to see if a score has been received yet. + @param get: AJAX get dictionary + @param system: Modulesystem (needed to align with other ajax functions) + @return: Returns the current state + """ state = self.state return {'state' : state} def save_answer(self, get, system): + """ + Saves a student answer + @param get: AJAX get dictionary + @param system: modulesystem + @return: Success indicator + """ if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and # seeing rubric. In normal use, students shouldn't see this because @@ -457,13 +515,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def update_score(self, get, system): """ - Delivers grading response (e.g. from asynchronous code checking) to - the capa problem, so its score can be updated - - 'get' must have a field 'response' which is a string that contains the - grader's response - - No ajax return is needed. Return empty dict. + Updates the current score via ajax. Called by xqueue. + Input: AJAX get dictionary, modulesystem + Output: None """ queuekey = get['queuekey'] score_msg = get['xqueue_body'] @@ -473,6 +527,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return dict() # No AJAX return is needed def get_html(self, system): + """ + Gets the HTML for this problem and renders it + Input: Modulesystem object + Output: Rendered HTML + """ #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() From f858dce753565e4555e875d01ea8909ff4599668 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:12:30 -0500 Subject: [PATCH 176/541] Document self assessment --- .../lib/xmodule/xmodule/open_ended_module.py | 14 +++++-- common/lib/xmodule/xmodule/openendedchild.py | 41 ++----------------- .../xmodule/xmodule/self_assessment_module.py | 37 +++++++++++++---- 3 files changed, 41 insertions(+), 51 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e4008309cd..ebd1cbfc02 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -39,6 +39,14 @@ log = logging.getLogger("mitx.courseware") class OpenEndedModule(openendedchild.OpenEndedChild): """ The open ended module supports all external open ended grader problems. + Sample XML file: + + + Enter essay here. + This is the answer. + {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + + """ def setup_response(self, system, location, definition, descriptor): """ @@ -562,7 +570,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ - Module for adding self assessment questions to courses + Module for adding open ended response questions to courses """ mako_template = "widgets/html-edit.html" module_class = OpenEndedModule @@ -578,12 +586,10 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): """ - Pull out the rubric, prompt, and submitmessage into a dictionary. + Pull out the open ended parameters into a dictionary. Returns: { - 'rubric': 'some-html', - 'prompt': 'some-html', 'oeparam': 'some-html' } """ diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 304271c620..5c2bdea76b 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -1,10 +1,3 @@ -""" -A Self Assessment module that allows students to write open-ended responses, -submit, then see a rubric and rate themselves. Persists student supplied -hints, answers, and assessment judgment (currently only correct/incorrect). -Parses xml definition file--see below for exact format. -""" - import copy from fs.errors import ResourceNotFoundError import itertools @@ -48,9 +41,9 @@ class OpenEndedChild(): initial (prompt, textbox shown) | - assessing (read-only textbox, rubric + assessment input shown) + assessing (read-only textbox, rubric + assessment input shown for self assessment, response queued for open ended) | - request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) + post_assessment (read-only textbox, read-only rubric and assessment, hint input box shown) | done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows a reset button that goes back to initial state. Saves previous @@ -69,6 +62,7 @@ class OpenEndedChild(): POST_ASSESSMENT = 'post_assessment' DONE = 'done' + #This is used to tell students where they are at in the module HUMAN_NAMES={ 'initial' : 'Started', 'assessing' : 'Being scored', @@ -78,35 +72,6 @@ class OpenEndedChild(): def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): - """ - Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, - and two optional attributes: - attempts, which should be an integer that defaults to 1. - If it's > 1, the student will be able to re-submit after they see - the rubric. - max_score, which should be an integer that defaults to 1. - It defines the maximum number of points a student can get. Assumed to be integer scale - from 0 to max_score, with an interval of 1. - - Note: all the submissions are stored. - - Sample file: - - - - Insert prompt text here. (arbitrary html) - - - Insert grading rubric here. (arbitrary html) - - - Please enter a hint below: (arbitrary html) - - - Thanks for submitting! (arbitrary html) - - - """ # Load instance state if instance_state is not None: diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 52701a8cf1..88632a38d0 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -1,10 +1,3 @@ -""" -A Self Assessment module that allows students to write open-ended responses, -submit, then see a rubric and rate themselves. Persists student supplied -hints, answers, and assessment judgment (currently only correct/incorrect). -Parses xml definition file--see below for exact format. -""" - import copy from fs.errors import ResourceNotFoundError import itertools @@ -31,14 +24,42 @@ import openendedchild log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): + """ + A Self Assessment module that allows students to write open-ended responses, + submit, then see a rubric and rate themselves. Persists student supplied + hints, answers, and assessment judgment (currently only correct/incorrect). + Parses xml definition file--see below for exact format. + Sample XML format: + + + What hint about this problem would you give to someone? + + + Save Succcesful. Thanks for participating! + + + """ def setup_response(self, system, location, definition, descriptor): + """ + Sets up the module + @param system: Modulesystem + @param location: location, to let the module know where it is. + @param definition: XML definition of the module. + @param descriptor: SelfAssessmentDescriptor + @return: None + """ self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] self.prompt = stringify_children(self.prompt) self.rubric = stringify_children(self.rubric) def get_html(self, system): + """ + Gets context and renders HTML that represents the module + @param system: Modulesystem + @return: Rendered HTML + """ #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() @@ -266,8 +287,6 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): Returns: { - 'rubric': 'some-html', - 'prompt': 'some-html', 'submitmessage': 'some-html' 'hintprompt': 'some-html' } From 4a2875ccdd302fa1d08990ed02912dd2e2182c0e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:15:57 -0500 Subject: [PATCH 177/541] Document open ended child --- common/lib/xmodule/xmodule/openendedchild.py | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5c2bdea76b..ce1b15074f 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -72,7 +72,6 @@ class OpenEndedChild(): def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): - # Load instance state if instance_state is not None: instance_state = json.loads(instance_state) @@ -102,6 +101,14 @@ class OpenEndedChild(): self.setup_response(system, location, definition, descriptor) def setup_response(self, system, location, definition, descriptor): + """ + Needs to be implemented by the inheritors of this module. Sets up additional fields used by the child modules. + @param system: Modulesystem + @param location: Module location + @param definition: XML definition + @param descriptor: Descriptor of the module + @return: None + """ pass def latest_answer(self): @@ -123,6 +130,11 @@ class OpenEndedChild(): return self.history[-1].get('post_assessment', "") def new_history_entry(self, answer): + """ + Adds a new entry to the history dictionary + @param answer: The student supplied answer + @return: None + """ self.history.append({'answer': answer}) def record_latest_score(self, score): @@ -213,12 +225,25 @@ class OpenEndedChild(): 'error': 'The problem state got out-of-sync'} def get_html(self): + """ + Needs to be implemented by inheritors. Renders the HTML that students see. + @return: + """ pass def handle_ajax(self): + """ + Needs to be implemented by child modules. Handles AJAX events. + @return: + """ pass def is_submission_correct(self, score): + """ + Checks to see if a given score makes the answer correct. Very naive right now (>66% is correct) + @param score: Numeric score. + @return: Boolean correct. + """ correct=False if(isinstance(score,(int, long, float, complex))): score_ratio = int(score) / float(self.max_score()) @@ -226,6 +251,10 @@ class OpenEndedChild(): return correct def is_last_response_correct(self): + """ + Checks to see if the last response in the module is correct. + @return: 'correct' if correct, otherwise 'incorrect' + """ score=self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' return correctness From 5a3a537c1b1668eec294966d5d1ea15cdc43543a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 13:00:37 -0500 Subject: [PATCH 178/541] Support formatting of peer grading feedback --- .../lib/xmodule/xmodule/open_ended_module.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ebd1cbfc02..0d30950592 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -422,8 +422,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild): log.error("External grader message is missing required tag: {0}" .format(tag)) return fail - - feedback = self._format_feedback(score_result) + #This is to support peer grading + if isinstance(score_result['score'], list): + feedback_items=[] + for i in xrange(0,len(score_result['score'])): + new_score_result={ + 'score' : score_result['score'][i], + 'feedback' : score_result['feedback'][i], + 'grader_type' : score_result['grader_type'], + 'success' : score_result['success'], + 'grader_id' : score_result['grader_id'][i], + 'submission_id' : score_result['submission_id'] + } + feedback_items.append(self._format_feedback(new_score_result)) + feedback="".join(feedback_items) + else: + feedback = self._format_feedback(score_result) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From c267efb40099d6fc2e3508257caa21e94378b800 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 13:03:01 -0500 Subject: [PATCH 179/541] Add in a comment --- common/lib/xmodule/xmodule/open_ended_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0d30950592..1ce1e09e03 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -437,6 +437,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) else: + #This is for instructor and ML grading feedback = self._format_feedback(score_result) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From c4b1c8d074bdb4efe1588acc474f26a2635e61c9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:29:49 -0500 Subject: [PATCH 180/541] Correct peer grading score parsing --- common/lib/xmodule/xmodule/open_ended_module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 1ce1e09e03..07ceb79b1a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -31,6 +31,7 @@ from capa.util import * import openendedchild from mitxmako.shortcuts import render_to_string +from numpy import median from datetime import datetime @@ -436,9 +437,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): } feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) + score = median(score_result['score']) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) + self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From 742d9475c79ae1faa7d6043e3597d70ece7cef11 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:31:02 -0500 Subject: [PATCH 181/541] Parse int from score --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 07ceb79b1a..ac852e4e5e 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -437,7 +437,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): } feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) - score = median(score_result['score']) + score = int(median(score_result['score'])) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) From 62e93870957c01e0e6b013103c2c06dee4bb6d4d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:42:29 -0500 Subject: [PATCH 182/541] Fix score passing from controller --- common/lib/xmodule/xmodule/open_ended_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ac852e4e5e..2a253c663f 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -441,11 +441,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) + score=score_result['score'] self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] - return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} + return {'valid' : True, 'score' : score, 'feedback' : feedback} def latest_post_assessment(self, short_feedback=False): """ From cb203a6f5532ec052c668fd5969261385fce56cc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 15:54:15 -0500 Subject: [PATCH 183/541] Better error messages --- .../xmodule/combined_open_ended_module.py | 1 + .../lib/xmodule/xmodule/open_ended_module.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4bc0c1fc85..42f2393ad9 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -224,6 +224,7 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.overwrite_state(current_task_state) self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + log.debug(current_task_state) return True def check_allow_reset(self): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 2a253c663f..367b5d9e67 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -405,23 +405,29 @@ class OpenEndedModule(openendedchild.OpenEndedChild): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) """ - fail = {'valid' : False, 'correct' : False, 'points' : 0, 'msg' : ''} + fail = {'valid' : False, 'score' : 0, 'feedback' : ''} try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) + error_message=("External grader message should be a JSON-serialized dict." + " Received score_msg = {0}".format(score_msg)) + log.error(error_message) + fail['feedback']=error_message return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) + error_message=("External grader message should be a JSON-serialized dict." + " Received score_result = {0}".format(score_result)) + log.error(error_message) + fail['feedback']=error_message return fail for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: if tag not in score_result: - log.error("External grader message is missing required tag: {0}" + error_message=("External grader message is missing required tag: {0}" .format(tag)) + log.error(error_message) + fail['feedback']=error _message return fail #This is to support peer grading if isinstance(score_result['score'], list): From f4968e1e8e1b80677130a662b5adb5c85c6a7f44 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:01:03 -0500 Subject: [PATCH 184/541] Fix spacing error --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 367b5d9e67..0420faf534 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -427,7 +427,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): error_message=("External grader message is missing required tag: {0}" .format(tag)) log.error(error_message) - fail['feedback']=error _message + fail['feedback']=error_message return fail #This is to support peer grading if isinstance(score_result['score'], list): From b0d3bcc5166feced1285826f745e42c8bd5ed871 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:37:15 -0500 Subject: [PATCH 185/541] Fix feedback response for peer grading so that students can respond to multiple feedback items --- .../capa/capa/templates/openendedinput.html | 56 ----------------- .../xmodule/combined_open_ended_module.py | 10 ++- .../js/src/combinedopenended/display.coffee | 62 ++++++++++--------- .../lib/xmodule/xmodule/open_ended_module.py | 11 ++-- 4 files changed, 48 insertions(+), 91 deletions(-) delete mode 100644 common/lib/capa/capa/templates/openendedinput.html diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html deleted file mode 100644 index c42ad73faf..0000000000 --- a/common/lib/capa/capa/templates/openendedinput.html +++ /dev/null @@ -1,56 +0,0 @@ -
    - - -
    - % if status == 'unsubmitted': - Unanswered - % elif status == 'correct': - Correct - % elif status == 'incorrect': - Incorrect - % elif status == 'queued': - Submitted for grading - % endif - - % if hidden: -
    - % endif -
    - - - - % if status == 'queued': - - % endif -
    - ${msg|n} - % if status in ['correct','incorrect']: -
    -
    - Respond to Feedback -
    -
    -

    How accurate do you find this feedback?

    -
    -
      -
    • -
    • -
    • -
    • -
    • -
    -
    -

    Additional comments:

    - -
    - -
    -
    -
    - % endif -
    -
    diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 42f2393ad9..244346625a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -336,8 +336,14 @@ class CombinedOpenEndedModule(XModule): last_post_assessment = task.latest_post_assessment() last_post_feedback="" if task_type=="openended": - last_post_assessment = task.latest_post_assessment(short_feedback=False) - last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) + last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) + if isinstance(last_post_assessment,list): + eval_list=[] + for i in xrange(0,len(last_post_assessment)): + eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) + last_post_evaluation="".join(eval_list) + else: + last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation last_correctness = task.is_last_response_correct() max_score = task.max_score() diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 8a5ef42270..5e2d2db86e 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -69,6 +69,39 @@ class @CombinedOpenEnded else @errors_area.html(response.error) + message_post: (event)=> + Logger.log 'message_post', @answers + external_grader_message=$(event.target).parent().parent().parent() + evaluation_scoring = $(event.target).parent() + + fd = new FormData() + feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value + submission_id = external_grader_message.find('div.submission_id')[0].innerHTML + grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() + + fd.append('feedback', feedback) + fd.append('submission_id', submission_id) + fd.append('grader_id', grader_id) + if(!score) + @gentle_alert "You need to pick a rating before you can submit." + return + else + fd.append('score', score) + + settings = + type: "POST" + data: fd + processData: false + contentType: false + success: (response) => + @gentle_alert response.msg + $('section.evaluation').slideToggle() + @message_wrapper.html(response.message_html) + + $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) + + rebind: () => # rebind to the appropriate function for the current state @submit_button.unbind('click') @@ -227,35 +260,6 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - message_post: => - Logger.log 'message_post', @answers - - fd = new FormData() - feedback = $('section.evaluation textarea.feedback-on-feedback')[0].value - submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML - grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML - score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() - fd.append('feedback', feedback) - fd.append('submission_id', submission_id) - fd.append('grader_id', grader_id) - if(!score) - @gentle_alert "You need to pick a rating before you can submit." - return - else - fd.append('score', score) - - settings = - type: "POST" - data: fd - processData: false - contentType: false - success: (response) => - @gentle_alert response.msg - $('section.evaluation').slideToggle() - @message_wrapper.html(response.message_html) - - $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) - gentle_alert: (msg) => if @el.find('.open-ended-alert').length @el.find('.open-ended-alert').remove() diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0420faf534..f715c9d76a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -391,7 +391,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return feedback_template - def _parse_score_msg(self, score_msg): + def _parse_score_msg(self, score_msg, join_feedback=True): """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, @@ -442,7 +442,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'submission_id' : score_result['submission_id'] } feedback_items.append(self._format_feedback(new_score_result)) - feedback="".join(feedback_items) + if join_feedback: + feedback="".join(feedback_items) + else: + feedback=feedback_items score = int(median(score_result['score'])) else: #This is for instructor and ML grading @@ -454,7 +457,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid' : True, 'score' : score, 'feedback' : feedback} - def latest_post_assessment(self, short_feedback=False): + def latest_post_assessment(self, short_feedback=False, join_feedback=True): """ Gets the latest feedback, parses, and returns @param short_feedback: If the long feedback is wanted or not @@ -463,7 +466,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not self.history: return "" - feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', "")) + feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback) if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' if feedback_dict['valid']: From e4568c3a2061b7ba17a6a1af45fac9f87e955762 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:41:17 -0500 Subject: [PATCH 186/541] Use include clearfix in css --- .../xmodule/xmodule/css/combinedopenended/display.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index be86757aee..8ebb3a2888 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -21,6 +21,7 @@ h2 { } section.combined-open-ended { + @include clearfix; .status-container { float:right; @@ -40,14 +41,6 @@ section.combined-open-ended { position:relative; } - &:after - { - content:"."; - display:block; - height:0; - visibility: hidden; - clear:both; - } } section.combined-open-ended-status { From ee2990da4f0c8151a00f2fb7ed401c44344d860c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:05:08 -0500 Subject: [PATCH 187/541] Change hidden div to input name --- common/lib/xmodule/xmodule/css/combinedopenended/display.scss | 1 - .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 8ebb3a2888..b5eb4b52e6 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -40,7 +40,6 @@ section.combined-open-ended { width: 93%; position:relative; } - } section.combined-open-ended-status { diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 5e2d2db86e..29b0424a6d 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('div.submission_id')[0].innerHTML - grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + submission_id = external_grader_message.find('input.submission_id')[0].innerHTML + grader_id = external_grader_message.find('input.grader_id')[0].innerHTML score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index f715c9d76a..3f5d5b0110 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From c76786ae4ee6ad63a88ed875e601884c24dcebdd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:08:10 -0500 Subject: [PATCH 188/541] Roll back hidden input changes --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 29b0424a6d..5e2d2db86e 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('input.submission_id')[0].innerHTML - grader_id = external_grader_message.find('input.grader_id')[0].innerHTML + submission_id = external_grader_message.find('div.submission_id')[0].innerHTML + grader_id = external_grader_message.find('div.grader_id')[0].innerHTML score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3f5d5b0110..f715c9d76a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From 8dbbb021a730256ef0ba3a9f04d58085d78ad9f2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:23:33 -0500 Subject: [PATCH 189/541] Change value passing to hidden input type --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 5e2d2db86e..2cbba143a3 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('div.submission_id')[0].innerHTML - grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + submission_id = external_grader_message.find('input.submission_id')[0].value + grader_id = external_grader_message.find('input.grader_id')[0].value score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index f715c9d76a..5649cbbd2c 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From 200493a54f4e7c4ffdd6a7c6d307098ff4aa7782 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 6 Jan 2013 20:57:44 +0000 Subject: [PATCH 190/541] instructor dashboard upgrade - add enrollment management --- ...ed__add_unique_courseenrollmentallowed_.py | 155 +++++++++++++ .../migrations/0021_remove_askbot.py.old | 157 ++++++++++++++ common/djangoapps/student/models.py | 18 ++ lms/djangoapps/instructor/views.py | 205 ++++++++++++++++-- lms/envs/dev.py | 4 + .../courseware/instructor_dashboard.html | 51 ++++- 6 files changed, 567 insertions(+), 23 deletions(-) create mode 100644 common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py create mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py new file mode 100644 index 0000000000..f7e2571685 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseEnrollmentAllowed' + db.create_table('student_courseenrollmentallowed', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + )) + db.send_create_signal('student', ['CourseEnrollmentAllowed']) + + # Adding unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.create_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.delete_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + # Deleting model 'CourseEnrollmentAllowed' + db.delete_table('student_courseenrollmentallowed') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] \ No newline at end of file diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old new file mode 100644 index 0000000000..89f7208f40 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_remove_askbot.py.old @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +ASKBOT_AUTH_USER_COLUMNS = ( + 'website', + 'about', + 'gold', + 'email_isvalid', + 'real_name', + 'location', + 'reputation', + 'gravatar', + 'bronze', + 'last_seen', + 'silver', + 'questions_per_page', + 'new_response_count', + 'seen_response_count', +) + + +class Migration(SchemaMigration): + + def forwards(self, orm): + "Kill the askbot" + # For MySQL, we're batching the alters together for performance reasons + if db.backend_name == 'mysql': + drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] + statement = "alter table `auth_user` {0};".format(", ".join(drops)) + db.execute(statement) + else: + for column in ASKBOT_AUTH_USER_COLUMNS: + db.delete_column('auth_user', column) + + def backwards(self, orm): + raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 2f5bc3ac04..d3254532bc 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -262,6 +262,24 @@ class CourseEnrollment(models.Model): return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) +class CourseEnrollmentAllowed(models.Model): + """ + Table of users (specified by email address strings) who are allowed to enroll in a specified course. + The user may or may not (yet) exist. Enrollment by users listed in this table is allowed + even if the enrollment time window is past. + """ + email = models.CharField(max_length=255, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + + class Meta: + unique_together = (('email', 'course_id'), ) + + def __unicode__(self): + return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) + + @receiver(post_save, sender=CourseEnrollment) def assign_default_role(sender, instance, **kwargs): if instance.user.is_staff: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2bad058ad8..b74fd495a1 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -2,8 +2,10 @@ from collections import defaultdict import csv +import json import logging import os +import requests import urllib from django.conf import settings @@ -20,7 +22,7 @@ from courseware.courses import get_course_with_access from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseEnrollmentAllowed from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -125,7 +127,7 @@ def instructor_dashboard(request, course_id): except Exception as err: msg += '

    Error: {0}

    '.format(escape(err)) - if action == 'Dump list of enrolled students': + if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) @@ -257,6 +259,70 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id), {}, page='idashboard') + #---------------------------------------- + # enrollment + + elif action == 'List students who may enroll but may not have yet signed up': + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + datatable = {'header': ['StudentEmail']} + datatable['data'] = [[x.email] for x in ceaset] + datatable['title'] = action + + elif action == 'Enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) + nce.save() + msg += "Enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) + nce.delete() + msg += "Un-enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll ALL students': + + ret = _do_enroll_students(course, course_id, '', overload=True) + datatable = ret['datatable'] + + elif action == 'Enroll multiple students': + + students = request.POST.get('enroll_multiple','') + ret = _do_enroll_students(course, course_id, students) + datatable = ret['datatable'] + + elif action == 'List sections available in remote gradebook': + + msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg += msg2 + + elif action in ['List students in section in remote gradebook', + 'Overload enrollment list using remote gradebook', + 'Merge enrollment list with remote gradebook']: + + section = request.POST.get('gradebook_section','') + msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg += msg2 + + if not 'List' in action: + students = ','.join([x['email'] for x in datatable['retdata']]) + overload = 'Overload' in action + ret = _do_enroll_students(course, course_id, students, overload=overload) + datatable = ret['datatable'] + + #---------------------------------------- # psychometrics @@ -270,9 +336,9 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) - #---------------------------------------- # context for rendering + context = {'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, @@ -285,16 +351,65 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') } return render_to_response('courseware/instructor_dashboard.html', context) + +def _do_remote_gradebook(course, action, args=None): + ''' + Perform remote gradebook action. Returns msg, datatable. + ''' + rg = course.metadata.get('remote_gradebook','') + if not rg: + msg = "No remote gradebook defined in course metadata" + return msg, {} + + rgurl = settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') + if not rgurl: + msg = "No remote gradebook url defined in settings.MITX_FEATURES" + return msg, {} + + rgname = rg.get('name','') + if not rgname: + msg = "No gradebook name defined in course remote_gradebook metadata" + return msg, {} + + if args is None: + args = {} + data = dict(submit=action, gradebook=rgname) + data.update(args) + + try: + resp = requests.post(rgurl, data=data, verify=False) + retdict = json.loads(resp.content) + except Exception as err: + msg = "Failed to communicate with gradebook server at %s
    " % rgurl + msg += "Error: %s" % err + msg += "
    resp=%s" % resp.content + msg += "
    data=%s" % data + return msg, {} + + msg = '
    %s
    ' % retdict['msg'].replace('\n','
    ') + retdata = retdict['data'] + + if retdata: + datatable = {'header': retdata[0].keys()} + datatable['data'] = [x.values() for x in retdata] + datatable['title'] = 'Remote gradebook response for %s' % action + datatable['retdata'] = retdata + else: + datatable = {} + + return msg, datatable + def _list_course_forum_members(course_id, rolename, datatable): ''' Fills in datatable with forum membership information, for a given role, so that it will be displayed on instructor dashboard. - course_ID = course's ID string + course_ID = the ID string for a course rolename = one of "Administrator", "Moderator", "Community TA" Returns message status string to append to displayed message, if role is unknown. @@ -455,6 +570,68 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +def _do_enroll_students(course, course_id, students, overload=False): + """Do the actual work of enrolling multiple students, presented as a string + of emails separated by commas or returns""" + + ns = [x.split('\n') for x in students.split(',')] + new_students = [item for sublist in ns for item in sublist] + new_students = [str(s.strip()) for s in new_students] + new_students_lc = [x.lower() for x in new_students] + + if '' in new_students: + new_students.remove('') + + status = dict([x,'unprocessed'] for x in new_students) + + if overload: # delete all but staff + todelete = CourseEnrollment.objects.filter(course_id=course_id) + for ce in todelete: + if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc: + status[ce.user.email] = 'deleted' + ce.delete() + else: + status[ce.user.email] = 'is staff' + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + for cea in ceaset: + status[cea.email] = 'removed from pending enrollment list' + ceaset.delete() + + for student in new_students: + try: + user=User.objects.get(email=student) + except User.DoesNotExist: + # user not signed up yet, put in pending enrollment allowed table + if CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id): + status[student] = 'user does not exist, enrollment already allowed, pending' + continue + cea = CourseEnrollmentAllowed(email=student, course_id=course_id) + cea.save() + status[student] = 'user does not exist, enrollment allowed, pending' + continue + + if CourseEnrollment.objects.filter(user=user, course_id=course_id): + status[student] = 'already enrolled' + continue + try: + nce = CourseEnrollment(user=user, course_id=course_id) + nce.save() + status[student] = 'added' + except: + status[student] = 'rejected' + + datatable = {'header': ['StudentEmail', 'action']} + datatable['data'] = [[x, status[x]] for x in status] + datatable['title'] = 'Enrollment of students' + + def sf(stat): return [x for x in status if status[x]==stat] + + data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'), + deleted=sf('deleted'), datatable=datatable) + + return data + + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def enroll_students(request, course_id): @@ -473,22 +650,10 @@ def enroll_students(request, course_id): course = get_course_with_access(request.user, course_id, 'staff') existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)] - if 'new_students' in request.POST: - new_students = request.POST['new_students'].split('\n') - else: - new_students = [] - new_students = [s.strip() for s in new_students] - - added_students = [] - rejected_students = [] - - for student in new_students: - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - added_students.append(student) - except: - rejected_students.append(student) + new_students = request.POST.get('new_students') + ret = _do_enroll_students(course, course_id, new_students) + added_students = ret['added'] + rejected_students = ret['rejected'] return render_to_response("enroll_students.html", {'course': course_id, 'existing_students': existing_students, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 058c67fa4d..f5999bf52e 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = { COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" +################################# mitx revision string ##################### + +MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip() + ################################# Staff grading config ##################### STAFF_GRADING_INTERFACE = { diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 74bc25fcbe..bb0dcef970 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -57,10 +57,13 @@ function goto( mode) Psychometrics | %endif Admin | - Forum Admin ] + Forum Admin | + Enrollment + ] -
    ${djangopid}
    +
    ${djangopid} + | ${mitx_version}
    @@ -163,10 +166,52 @@ function goto( mode) %endif %endif +##----------------------------------------------------------------------------- +%if modeflag.get('Enrollment'): + +
    +

    + + +

    + Student Email: + + +


    + + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

    Pull enrollment from remote gradebook

    +
      +
    • Gradebook name:
    • +
    • Section:
    • +
    + + + + +
    + + %endif + +

    Add students: enter emails, separated by returns or commas;

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

    From 97fb05444922c6def563e5fb56c0f3d4599ea0a4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 00:50:59 +0000 Subject: [PATCH 191/541] add export grades to remote gradebook to instructor dashboard --- lms/djangoapps/instructor/views.py | 96 ++++++++++++++++--- .../courseware/instructor_dashboard.html | 38 +++++++- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b74fd495a1..b5caeac964 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -8,6 +8,8 @@ import os import requests import urllib +from StringIO import StringIO + from django.conf import settings from django.contrib.auth.models import User, Group from django.http import HttpResponse @@ -77,9 +79,12 @@ def instructor_dashboard(request, course_id): data.append(['metadata', escape(str(course.metadata))]) datatable['data'] = data - def return_csv(fn, datatable): - response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + def return_csv(fn, datatable, fp=None): + if fp is None: + response = HttpResponse(mimetype='text/csv') + response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + else: + response = fp writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: @@ -160,6 +165,65 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) + #---------------------------------------- + # export grades to remote gradebook + + elif action=='List assignments available in remote gradebook': + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') + msg += msg2 + + elif action=='List assignments available for this course': + log.debug(action) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + + assignments = [[x] for x in allgrades['assignments']] + datatable = {'header': ['Assignment Name']} + datatable['data'] = assignments + datatable['title'] = action + + msg += 'assignments=
    %s
    ' % assignments + + elif action=='List enrolled students matching remote gradebook': + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') + datatable = {'header': ['Student email', 'Match?']} + rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] + def domatch(x): + return 'yes' if x.email in rg_students else 'No' + datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] + datatable['title'] = action + + elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', + 'Export CSV file of grades for assignment']: + + log.debug(action) + datatable = {} + aname = request.POST.get('assignment_name','') + if not aname: + msg += "Please enter an assignment name" + else: + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + if aname not in allgrades['assignments']: + msg += "Invalid assignment name '%s'" % aname + else: + aidx = allgrades['assignments'].index(aname) + datatable = {'header': ['External email', aname]} + datatable['data'] = [[x.email, x.grades[aidx]] for x in allgrades['students']] + datatable['title'] = 'Grades for assignment "%s"' % aname + + if 'Export CSV' in action: + # generate and return CSV file + return return_csv('grades %s.csv' % aname, datatable) + + elif 'remote gradebook' in action: + fp = StringIO() + return_csv('', datatable, fp=fp) + fp.seek(0) + files = {'datafile': fp} + msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files) + msg += msg2 + + #---------------------------------------- # Admin @@ -305,7 +369,7 @@ def instructor_dashboard(request, course_id): elif action == 'List sections available in remote gradebook': - msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', @@ -313,7 +377,7 @@ def instructor_dashboard(request, course_id): 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section','') - msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section) ) msg += msg2 if not 'List' in action: @@ -357,7 +421,7 @@ def instructor_dashboard(request, course_id): return render_to_response('courseware/instructor_dashboard.html', context) -def _do_remote_gradebook(course, action, args=None): +def _do_remote_gradebook(user, course, action, args=None, files=None): ''' Perform remote gradebook action. Returns msg, datatable. ''' @@ -378,11 +442,11 @@ def _do_remote_gradebook(course, action, args=None): if args is None: args = {} - data = dict(submit=action, gradebook=rgname) + data = dict(submit=action, gradebook=rgname, user=user.email) data.update(args) try: - resp = requests.post(rgurl, data=data, verify=False) + resp = requests.post(rgurl, data=data, verify=False, files=files) retdict = json.loads(resp.content) except Exception as err: msg = "Failed to communicate with gradebook server at %s
    " % rgurl @@ -392,7 +456,7 @@ def _do_remote_gradebook(course, action, args=None): return msg, {} msg = '
    %s
    ' % retdict['msg'].replace('\n','
    ') - retdata = retdict['data'] + retdata = retdict['data'] # a list of dicts if retdata: datatable = {'header': retdata[0].keys()} @@ -495,16 +559,18 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] + assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: - header += [score.section for score in gradeset['raw_scores']] + assignments += [score.section for score in gradeset['raw_scores']] else: - header += [x['label'] for x in gradeset['section_breakdown']] + assignments += [x['label'] for x in gradeset['section_breakdown']] + header += assignments - datatable = {'header': header} + datatable = {'header': header, 'assignments': assignments, 'students': enrolled_students} data = [] for student in enrolled_students: @@ -518,9 +584,11 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - datarow += [score.earned for score in gradeset['raw_scores']] + student_grades = [score.earned for score in gradeset['raw_scores']] else: - datarow += [x['percent'] for x in gradeset['section_breakdown']] + student_grades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += student_grades + student.grades = student_grades # store in student object data.append(datarow) datatable['data'] = data diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index bb0dcef970..b2ec220484 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -96,6 +96,42 @@ function goto( mode)

    +
    + + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

    Export grades to remote gradebook

    +

    The assignments defined for this course should match the ones + stored in the gradebook, for this to work properly!

    + +
      +
    • Gradebook name: ${rg.get('name','None defined!')} +
      +
      + + +
      +
      +
    • +
    • +
      +
      +
    • +
    • Assignment name: +
      +
      + + + +
    • +
    + + %endif + %endif ##----------------------------------------------------------------------------- @@ -187,7 +223,7 @@ function goto( mode)

    Pull enrollment from remote gradebook

      -
    • Gradebook name:
    • +
    • Gradebook name: ${rg.get('name','None defined!')}
    • Section:
    From 82e31d533b1640836b683e9f408b8d9afb5cc6e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 02:03:27 +0000 Subject: [PATCH 192/541] Hookup CourseEnrollmentAllowed to lms/djangoapps/courseware/access.py --- lms/djangoapps/courseware/access.py | 6 ++++++ lms/djangoapps/instructor/views.py | 23 ++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ba9b8a3bc0..0d4a37eda5 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,6 +5,8 @@ like DISABLE_START_DATES""" import logging import time +import student.models + from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -124,6 +126,10 @@ def _has_access_course_desc(user, course, action): debug("Allow: in enrollment period") return True + # if user is in CourseEnrollmentAllowed with right course_id then can also enroll + if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True + # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b5caeac964..0ea3cf0435 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -335,26 +335,27 @@ def instructor_dashboard(request, course_id): elif action == 'Enroll student': student = request.POST.get('enstudent','') - datatable = {} - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - msg += "Enrolled student with email '%s'" % student - except Exception as err: - msg += "Error! Failed to enroll student with email '%s'\n" % student - msg += str(err) + '\n' + ret = _do_enroll_students(course, course_id, student) + datatable = ret['datatable'] elif action == 'Un-enroll student': student = request.POST.get('enstudent','') datatable = {} + isok = False + cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student) + if cea: + cea.delete() + msg += "Un-enrolled student with email '%s'" % student + isok = True try: nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) nce.delete() msg += "Un-enrolled student with email '%s'" % student except Exception as err: - msg += "Error! Failed to un-enroll student with email '%s'\n" % student - msg += str(err) + '\n' + if not isok: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' elif action == 'Un-enroll ALL students': @@ -582,7 +583,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, if get_grades: gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) - # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) + log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: student_grades = [score.earned for score in gradeset['raw_scores']] else: From 04d6f08c0cd4c901649c90edc0bca71424e2af7b Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:17:02 +0000 Subject: [PATCH 193/541] add offline grade computation & DB table for this --- common/djangoapps/student/admin.py | 2 + lms/djangoapps/courseware/access.py | 14 ++- lms/djangoapps/courseware/admin.py | 5 + ..._add_unique_offlinecomputedgrade_user_c.py | 117 ++++++++++++++++++ lms/djangoapps/courseware/models.py | 37 ++++++ .../management/commands/compute_grades.py | 50 ++++++++ .../instructor/offline_gradecalc.py | 103 +++++++++++++++ lms/djangoapps/instructor/views.py | 56 ++++++--- .../courseware/instructor_dashboard.html | 6 + 9 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py create mode 100644 lms/djangoapps/instructor/management/commands/compute_grades.py create mode 100644 lms/djangoapps/instructor/offline_gradecalc.py diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index ec3b708ca7..64fe844801 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -12,6 +12,8 @@ admin.site.register(UserTestGroup) admin.site.register(CourseEnrollment) +admin.site.register(CourseEnrollmentAllowed) + admin.site.register(Registration) admin.site.register(PendingNameChange) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 0d4a37eda5..b58f8d5470 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,8 +5,6 @@ like DISABLE_START_DATES""" import logging import time -import student.models - from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -15,6 +13,13 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor +# student.models imports Role, which imports courseware.access ; use a try, to break the circular import +try: + from student.models import CourseEnrollmentAllowed +except Exception as err: + CourseEnrollmentAllowed = None + + DEBUG_ACCESS = False log = logging.getLogger(__name__) @@ -127,8 +132,9 @@ def _has_access_course_desc(user, course, action): return True # if user is in CourseEnrollmentAllowed with right course_id then can also enroll - if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): - return True + if user is not None and CourseEnrollmentAllowed: + if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py index cda4fbb788..f7e54d1800 100644 --- a/lms/djangoapps/courseware/admin.py +++ b/lms/djangoapps/courseware/admin.py @@ -7,3 +7,8 @@ from django.contrib import admin from django.contrib.auth.models import User admin.site.register(StudentModule) + +admin.site.register(OfflineComputedGrade) + +admin.site.register(OfflineComputedGradeLog) + diff --git a/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py new file mode 100644 index 0000000000..674f97cec8 --- /dev/null +++ b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'OfflineComputedGrade' + db.create_table('courseware_offlinecomputedgrade', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + ('gradeset', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('courseware', ['OfflineComputedGrade']) + + # Adding unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.create_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Adding model 'OfflineComputedGradeLog' + db.create_table('courseware_offlinecomputedgradelog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('seconds', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('nstudents', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('courseware', ['OfflineComputedGradeLog']) + + + def backwards(self, orm): + # Removing unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.delete_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Deleting model 'OfflineComputedGrade' + db.delete_table('courseware_offlinecomputedgrade') + + # Deleting model 'OfflineComputedGradeLog' + db.delete_table('courseware_offlinecomputedgradelog') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'courseware.offlinecomputedgrade': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'courseware.offlinecomputedgradelog': { + 'Meta': {'object_name': 'OfflineComputedGradeLog'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'courseware.studentmodule': { + 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}), + 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), + 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}), + 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['courseware'] \ No newline at end of file diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index ffc7c929de..21ef8b3d66 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -177,3 +177,40 @@ class StudentModuleCache(object): def append(self, student_module): self.cache.append(student_module) + + +class OfflineComputedGrade(models.Model): + """ + Table of grades computed offline for a given user and course. + """ + user = models.ForeignKey(User, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + updated = models.DateTimeField(auto_now=True, db_index=True) + + gradeset = models.TextField(null=True, blank=True) # grades, stored as JSON + + class Meta: + unique_together = (('user', 'course_id'), ) + + def __unicode__(self): + return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset) + + +class OfflineComputedGradeLog(models.Model): + """ + Log of when offline grades are computed. + Use this to be able to show instructor when the last computed grades were done. + """ + class Meta: + ordering = ["-created"] + get_latest_by = "created" + + course_id = models.CharField(max_length=255, db_index=True) + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + seconds = models.IntegerField(default=0) # seconds elapsed for computation + nstudents = models.IntegerField(default=0) + + def __unicode__(self): + return "[OCGLog] %s: %s" % (self.course_id, self.created) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py new file mode 100644 index 0000000000..717bfd5802 --- /dev/null +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# +# django management command: dump grades to csv files +# for use by batch processes + +import os, sys, string +import datetime +import json + +#import student.models +from instructor.offline_gradecalc import * +from courseware.courses import get_course_by_id +from xmodule.modulestore.django import modulestore + +from django.conf import settings +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Compute grades for all students in a course, and store result in DB.\n" + help += "Usage: compute_grades course_id_or_dir \n" + help += " course_id_or_dir: either course_id or course_dir\n" + + def handle(self, *args, **options): + + print "args = ", args + + course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' + + if len(args)>0: + course_id = args[0] + + try: + course = get_course_by_id(course_id) + except Exception as err: + if course_id in modulestore().courses: + course = modulestore().courses[course_id] + else: + print "-----------------------------------------------------------------------------" + print "Sorry, cannot find course %s" % course_id + print "Please provide a course ID or course data directory name, eg content-mit-801rq" + return + + print "-----------------------------------------------------------------------------" + print "Computing grades for %s" % (course.id) + + offline_grade_calculation(course.id) + + + + diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py new file mode 100644 index 0000000000..7c102805b4 --- /dev/null +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -0,0 +1,103 @@ +# ======== Offline calculation of grades ============================================================================= +# +# Computing grades of a large number of students can take a long time. These routines allow grades to +# be computed offline, by a batch process (eg cronjob). +# +# The grades are stored in the OfflineComputedGrade table of the courseware model. + +import json +import logging +import time + +import courseware.models + +from collections import namedtuple +from json import JSONEncoder +from courseware import grades, models +from courseware.courses import get_course_by_id +from django.contrib.auth.models import User, Group + + +class MyEncoder(JSONEncoder): + + def _iterencode(self, obj, markers=None): + if isinstance(obj, tuple) and hasattr(obj, '_asdict'): + gen = self._iterencode_dict(obj._asdict(), markers) + else: + gen = JSONEncoder._iterencode(self, obj, markers) + for chunk in gen: + yield chunk + + +def offline_grade_calculation(course_id): + ''' + Compute grades for all students for a specified course, and save results to the DB. + ''' + + tstart = time.time() + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') + + enc = MyEncoder() + + class DummyRequest(object): + META = {} + def __init__(self): + return + def get_host(self): + return 'edx.mit.edu' + def is_secure(self): + return False + + request = DummyRequest() + + print "%d enrolled students" % len(enrolled_students) + course = get_course_by_id(course_id) + + for student in enrolled_students: + gradeset = grades.grade(student, request, course, keep_raw_scores=True) + gs = enc.encode(gradeset) + ocg, created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_id) + ocg.gradeset = gs + ocg.save() + print "%s done" % student # print statement used because this is run by a management command + + tend = time.time() + dt = tend - tstart + + ocgl = models.OfflineComputedGradeLog(course_id=course_id, seconds=dt, nstudents=len(enrolled_students)) + ocgl.save() + print ocgl + print "All Done!" + + +def offline_grades_available(course_id): + ''' + Returns False if no offline grades available for specified course. + Otherwise returns latest log field entry about the available pre-computed grades. + ''' + ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_id) + if not ocgl: + return False + return ocgl.latest('created') + + +def student_grades(student, request, course, keep_raw_scores=False, use_offline=False): + ''' + This is the main interface to get grades. It has the same parameters as grades.grade, as well + as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB. + ''' + + if not use_offline: + return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores) + + try: + ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id) + except models.OfflineComputedGrade.DoesNotExist: + return dict(raw_scores=[], section_breakdown=[], + msg='Error: no offline gradeset available for %s, %s' % (student, course.id)) + + return json.loads(ocg.gradeset) + + + + diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0ea3cf0435..2e8db884ff 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,8 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views - +from .grading import StaffGrading +from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) @@ -103,6 +104,7 @@ def instructor_dashboard(request, course_id): # process actions from form POST action = request.POST.get('action', '') + use_offline = request.POST.get('use_offline_grades',False) if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: @@ -134,32 +136,32 @@ def instructor_dashboard(request, course_id): if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'list-students', {}, page='idashboard') elif 'Dump Grades' in action: log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades', {}, page='idashboard') elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, - get_raw_scores=True) + get_raw_scores=True, use_offline=use_offline) datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard') elif 'Download CSV of all student grades' in action: track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard') return return_csv('grades_{0}.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id)) + get_student_grade_summary_data(request, course, course_id, use_offline=use_offline)) elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard') return return_csv('grades_{0}_raw.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id, get_raw_scores=True)) + get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') @@ -174,7 +176,7 @@ def instructor_dashboard(request, course_id): elif action=='List assignments available for this course': log.debug(action) - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': ['Assignment Name']} @@ -184,7 +186,7 @@ def instructor_dashboard(request, course_id): msg += 'assignments=
    %s
    ' % assignments elif action=='List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] @@ -202,7 +204,7 @@ def instructor_dashboard(request, course_id): if not aname: msg += "Please enter an assignment name" else: - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "Invalid assignment name '%s'" % aname else: @@ -401,6 +403,12 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) + #---------------------------------------- + # offline grades? + + if use_offline: + msg += "
    Grades from %s" % offline_grades_available(course_id) + #---------------------------------------- # context for rendering @@ -416,7 +424,8 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), - 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING',''), + 'offline_grade_log' : offline_grades_available(course_id), } return render_to_response('courseware/instructor_dashboard.html', context) @@ -539,7 +548,7 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): return msg -def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False): +def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False): ''' Return data arrays with student identity and grades for specified course. @@ -563,7 +572,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header - gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: assignments += [score.section for score in gradeset['raw_scores']] @@ -582,20 +591,22 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, datarow.append('') if get_grades: - gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - student_grades = [score.earned for score in gradeset['raw_scores']] + # TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned'] + sgrades = [(getattr(score,'earned','') or score[0]) for score in gradeset['raw_scores']] else: - student_grades = [x['percent'] for x in gradeset['section_breakdown']] - datarow += student_grades - student.grades = student_grades # store in student object + sgrades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += sgrades + student.grades = sgrades # store in student object data.append(datarow) datatable['data'] = data return datatable - +#----------------------------------------------------------------------------- +# Staff grading @@ -616,7 +627,7 @@ def gradebook(request, course_id): student_info = [{'username': student.username, 'id': student.id, 'email': student.email, - 'grade_summary': grades.grade(student, request, course), + 'grade_summary': student_grades(student, request, course), 'realname': student.profile.name, } for student in enrolled_students] @@ -639,6 +650,10 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +#----------------------------------------------------------------------------- +# enrollment + + def _do_enroll_students(course, course_id, students, overload=False): """Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns""" @@ -731,6 +746,9 @@ def enroll_students(request, course_id): 'debug': new_students}) +#----------------------------------------------------------------------------- +# answer distribution + def get_answers_distribution(request, course_id): """ Get the distribution of answers for all graded problems in the course. diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index b2ec220484..235505fc29 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -71,6 +71,12 @@ function goto( mode) ##----------------------------------------------------------------------------- %if modeflag.get('Grades'): + + %if offline_grade_log: +

    Pre-computed grades ${offline_grade_log} available: Use? +

    + %endif +

    Gradebook

    From 16b91cf73277e900ad746deba1ba25bd0a027f34 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:35:55 +0000 Subject: [PATCH 194/541] remove spurious old migration file --- .../migrations/0021_remove_askbot.py.old | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old deleted file mode 100644 index 89f7208f40..0000000000 --- a/common/djangoapps/student/migrations/0021_remove_askbot.py.old +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -ASKBOT_AUTH_USER_COLUMNS = ( - 'website', - 'about', - 'gold', - 'email_isvalid', - 'real_name', - 'location', - 'reputation', - 'gravatar', - 'bronze', - 'last_seen', - 'silver', - 'questions_per_page', - 'new_response_count', - 'seen_response_count', -) - - -class Migration(SchemaMigration): - - def forwards(self, orm): - "Kill the askbot" - # For MySQL, we're batching the alters together for performance reasons - if db.backend_name == 'mysql': - drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] - statement = "alter table `auth_user` {0};".format(", ".join(drops)) - db.execute(statement) - else: - for column in ASKBOT_AUTH_USER_COLUMNS: - db.delete_column('auth_user', column) - - def backwards(self, orm): - raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'student.courseenrollment': { - 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'student.pendingemailchange': { - 'Meta': {'object_name': 'PendingEmailChange'}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.pendingnamechange': { - 'Meta': {'object_name': 'PendingNameChange'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.registration': { - 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.testcenteruser': { - 'Meta': {'object_name': 'TestCenterUser'}, - 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), - 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), - 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), - 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), - 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), - 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), - 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), - 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), - 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) - }, - 'student.userprofile': { - 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, - 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), - 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), - 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) - }, - 'student.usertestgroup': { - 'Meta': {'object_name': 'UserTestGroup'}, - 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) - } - } - - complete_apps = ['student'] From e5c958082bff84c79bde4e979828796813335d72 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 5 Jan 2013 21:08:35 +0000 Subject: [PATCH 195/541] list/add/delete instructors on instructor dashboard --- lms/djangoapps/courseware/access.py | 14 ++++-- lms/djangoapps/instructor/views.py | 50 +++++++++++++++++-- .../courseware/instructor_dashboard.html | 10 ++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index b58f8d5470..60fcbff3a3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -171,13 +171,19 @@ def _has_access_course_desc(user, course, action): return _dispatch(checkers, action, user, course) + def _get_access_group_name_course_desc(course, action): ''' - Return name of group which gives staff access to course. Only understands action = 'staff' + Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor' ''' - if not action=='staff': - return [] - return _course_staff_group_name(course.location) + if action=='staff': + return _course_staff_group_name(course.location) + elif action=='instructor': + return _course_instructor_group_name(course.location) + + return [] + + def _has_access_error_desc(user, descriptor, action): """ diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8db884ff..2e8b9fd6bc 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -94,11 +94,17 @@ def instructor_dashboard(request, course_id): return response def get_staff_group(course): - staffgrp = get_access_group_name(course, 'staff') + return get_group(course, 'staff') + + def get_instructor_group(course): + return get_group(course, 'instructor') + + def get_group(course, groupname): + grpname = get_access_group_name(course, groupname) try: - group = Group.objects.get(name=staffgrp) + group = Group.objects.get(name=grpname) except Group.DoesNotExist: - group = Group(name=staffgrp) # create the group + group = Group(name=grpname) # create the group group.save() return group @@ -239,6 +245,16 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') + elif 'List course instructors' in action: + group = get_instructor_group(course) + msg += 'Instructor group = {0}'.format(group.name) + log.debug('instructor grp={0}'.format(group.name)) + uset = group.user_set.all() + datatable = {'header': ['Username', 'Full name']} + datatable['data'] = [[x.username, x.profile.name] for x in uset] + datatable['title'] = 'List of Instructors in course {0}'.format(course_id) + track.views.server_track(request, 'list-instructors', {}, page='idashboard') + elif action == 'Add course staff': uname = request.POST['staffuser'] try: @@ -253,6 +269,20 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Add instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Added {0} to instructor group = {1}'.format(user, group.name) + log.debug('staffgrp={0}'.format(group.name)) + user.groups.add(group) + track.views.server_track(request, 'add-instructor {0}'.format(user), {}, page='idashboard') + elif action == 'Remove course staff': uname = request.POST['staffuser'] try: @@ -267,6 +297,20 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Remove instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) + log.debug('instructorgrp={0}'.format(group.name)) + user.groups.remove(group) + track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') + #---------------------------------------- # forum administration diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 235505fc29..7f1912cd45 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -173,6 +173,16 @@ function goto( mode)
    %endif + %if admin_access: +
    +

    + +

    + + +


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

    From ec94f7328ce77d61329c0b477da52ae8a5037893 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:13:05 -0500 Subject: [PATCH 196/541] remove try...except workaround for circular import bug fixed by https://github.com/MITx/mitx/issues/1260 --- lms/djangoapps/courseware/access.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 60fcbff3a3..26f9fcdfd3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -13,12 +13,7 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor -# student.models imports Role, which imports courseware.access ; use a try, to break the circular import -try: - from student.models import CourseEnrollmentAllowed -except Exception as err: - CourseEnrollmentAllowed = None - +from student.models import CourseEnrollmentAllowed DEBUG_ACCESS = False From 9853c6323f5c3f7d6b65dce6126e647275d56e8b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 10 Jan 2013 15:19:38 -0500 Subject: [PATCH 197/541] Remove circular dependencies that connect student.models and django_comment_client Includes removal of "from django_comment_client.models import Role" from common/djangoapps/student/models.py Conflicts: common/djangoapps/student/models.py --- common/djangoapps/student/models.py | 11 ----------- lms/djangoapps/django_comment_client/models.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index d3254532bc..d9ce790ebe 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -49,7 +49,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver import comment_client as cc -from django_comment_client.models import Role log = logging.getLogger(__name__) @@ -280,16 +279,6 @@ class CourseEnrollmentAllowed(models.Model): return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) -@receiver(post_save, sender=CourseEnrollment) -def assign_default_role(sender, instance, **kwargs): - if instance.user.is_staff: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] - else: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] - - logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) - instance.user.roles.add(role) - #cache_relation(User.profile) #### Helper methods for use from python manage.py shell. diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 628ac21a4a..a6a2c23603 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -2,6 +2,10 @@ import logging from django.db import models from django.contrib.auth.models import User +from django.dispatch import receiver +from django.db.models.signals import post_save + +from student.models import CourseEnrollment from courseware.courses import get_course_by_id @@ -45,3 +49,14 @@ class Permission(models.Model): def __unicode__(self): return self.name + + +@receiver(post_save, sender=CourseEnrollment) +def assign_default_role(sender, instance, **kwargs): + if instance.user.is_staff: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] + else: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] + + logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) + instance.user.roles.add(role) From 5cc88ec1adbc04b4a6459c8acd90209bb2e5adc9 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:25:29 -0500 Subject: [PATCH 198/541] example course_id for compute_grades management command --- .../instructor/management/commands/compute_grades.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 717bfd5802..462833ba3c 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -19,15 +19,17 @@ class Command(BaseCommand): help = "Compute grades for all students in a course, and store result in DB.\n" help += "Usage: compute_grades course_id_or_dir \n" help += " course_id_or_dir: either course_id or course_dir\n" + help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' def handle(self, *args, **options): print "args = ", args - course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' - if len(args)>0: course_id = args[0] + else: + print self.help + return try: course = get_course_by_id(course_id) From 37f848949d80a0dc8aea45e6d8f7f560bbb61628 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:29:52 -0500 Subject: [PATCH 199/541] only is_staff users can add/edit/delete course instructors --- lms/djangoapps/instructor/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8b9fd6bc..07dbfacc64 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -245,7 +245,7 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') - elif 'List course instructors' in action: + elif 'List course instructors' in action and request.user.is_staff: group = get_instructor_group(course) msg += 'Instructor group = {0}'.format(group.name) log.debug('instructor grp={0}'.format(group.name)) @@ -269,7 +269,7 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Add instructor': + elif action == 'Add instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) @@ -297,7 +297,7 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Remove instructor': + elif action == 'Remove instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) From ed96046ad77e609330968834eeee8e4e6a573037 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:59:20 -0500 Subject: [PATCH 200/541] add documentation on remote gradebook xserver API --- doc/remote_gradebook.md | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 doc/remote_gradebook.md diff --git a/doc/remote_gradebook.md b/doc/remote_gradebook.md new file mode 100644 index 0000000000..3743e98753 --- /dev/null +++ b/doc/remote_gradebook.md @@ -0,0 +1,47 @@ +Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers". + +1. Definitions + +An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages. + +"Stellar" is the MIT on-campus gradebook system. + +2. Setup + +The remote gradebook xserver should be specified in the lms.envs configuration using + + MITX_FEATURES[REMOTE_GRADEBOOK_URL] + +Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg: + + "remote_gradebook": { + "name" : "STELLAR:/project/mitxdemosite", + "section" : "r01" + }, + +3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields: + + - submit: get-assignments, get-membership, post-grades, or get-sections + - gradebook: name of gradebook + - user: username of staff person initiating the request (for logging) + - section: (optional) name of section + +The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard. + +The data is a list of dicts (associative arrays). Each dict should be key:value. + +## For submit=post-grades: + +A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment). + +## For submit=get-assignments + +data keys = "AssignmentName" + +## For submit=get-membership + +data keys = "email", "name", "section" + +## For submit=get-sections + +data keys = "SectionName" From b7ad39a0b96de070776933ed96690ff4d3f9b35b Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:09:46 -0500 Subject: [PATCH 201/541] instructor dashboard shouldn't import .grading anymore --- lms/djangoapps/instructor/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 07dbfacc64..3a3380407d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -from .grading import StaffGrading from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) From 4f869ad38258a8664344188c5e20fa8a328311e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:10:52 -0500 Subject: [PATCH 202/541] remove spurious comment --- lms/djangoapps/instructor/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 3a3380407d..2d58799efe 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -649,9 +649,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, return datatable #----------------------------------------------------------------------------- -# Staff grading - - @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): From bcbf65e2b9f1108452f68136d802796463b1f974 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 09:45:10 -0500 Subject: [PATCH 203/541] Code reformat, line length fix, change text names to variables --- .../xmodule/combined_open_ended_module.py | 282 +++++++++--------- .../lib/xmodule/xmodule/open_ended_module.py | 214 ++++++------- common/lib/xmodule/xmodule/openendedchild.py | 22 +- .../xmodule/xmodule/self_assessment_module.py | 18 +- 4 files changed, 275 insertions(+), 261 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 244346625a..358a3b6995 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -45,14 +45,14 @@ class CombinedOpenEndedModule(XModule): # states INITIAL = 'initial' ASSESSING = 'assessing' - INTERMEDIATE_DONE='intermediate_done' + INTERMEDIATE_DONE = 'intermediate_done' DONE = 'done' - TASK_TYPES=["self", "ml", "instructor", "peer"] + TASK_TYPES = ["self", "ml", "instructor", "peer"] js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'), - ]} + ]} js_module_name = "CombinedOpenEnded" css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} @@ -88,7 +88,8 @@ class CombinedOpenEndedModule(XModule): Enter essay here. This is the answer. - {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + {"grader_settings" : "ml_grading.conf", + "problem_id" : "6.002x/Welcome/OETest"} @@ -108,9 +109,9 @@ class CombinedOpenEndedModule(XModule): #Tells the system which xml definition to load self.current_task_number = instance_state.get('current_task_number', 0) #This loads the states of the individual children - self.task_states= instance_state.get('task_states', []) + self.task_states = instance_state.get('task_states', []) #Overall state of the combined open ended module - self.state = instance_state.get('state', 'initial') + self.state = instance_state.get('state', self.INITIAL) self.attempts = instance_state.get('attempts', 0) @@ -124,13 +125,13 @@ class CombinedOpenEndedModule(XModule): #Static data is passed to the child modules to render self.static_data = { - 'max_score' : self._max_score, - 'max_attempts' : self.max_attempts, - 'prompt' : definition['prompt'], - 'rubric' : definition['rubric'] + 'max_score': self._max_score, + 'max_attempts': self.max_attempts, + 'prompt': definition['prompt'], + 'rubric': definition['rubric'] } - self.task_xml=definition['task_xml'] + self.task_xml = definition['task_xml'] self.setup_next_task() def get_tag_name(self, xml): @@ -139,7 +140,7 @@ class CombinedOpenEndedModule(XModule): Input: XML string Output: The name of the root tag """ - tag=etree.fromstring(xml).tag + tag = etree.fromstring(xml).tag return tag def overwrite_state(self, current_task_state): @@ -149,15 +150,15 @@ class CombinedOpenEndedModule(XModule): Input: Task state json string Output: Task state json string """ - last_response_data=self.get_last_response(self.current_task_number-1) + last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] - loaded_task_state=json.loads(current_task_state) - if loaded_task_state['state']== self.INITIAL: - loaded_task_state['state']=self.ASSESSING + loaded_task_state = json.loads(current_task_state) + if loaded_task_state['state'] == self.INITIAL: + loaded_task_state['state'] = self.ASSESSING loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) - current_task_state=json.dumps(loaded_task_state) + loaded_task_state['history'].append({'answer': last_response}) + current_task_state = json.dumps(loaded_task_state) return current_task_state def child_modules(self): @@ -167,17 +168,17 @@ class CombinedOpenEndedModule(XModule): Input: None Output: A dictionary of dictionaries containing the descriptor functions and module functions """ - child_modules={ - 'openended' : open_ended_module.OpenEndedModule, - 'selfassessment' : self_assessment_module.SelfAssessmentModule, + child_modules = { + 'openended': open_ended_module.OpenEndedModule, + 'selfassessment': self_assessment_module.SelfAssessmentModule, } - child_descriptors={ - 'openended' : open_ended_module.OpenEndedDescriptor, - 'selfassessment' : self_assessment_module.SelfAssessmentDescriptor, + child_descriptors = { + 'openended': open_ended_module.OpenEndedDescriptor, + 'selfassessment': self_assessment_module.SelfAssessmentDescriptor, } - children={ - 'modules' : child_modules, - 'descriptors' : child_descriptors, + children = { + 'modules': child_modules, + 'descriptors': child_descriptors, } return children @@ -188,41 +189,47 @@ class CombinedOpenEndedModule(XModule): Input: A boolean indicating whether or not the reset function is calling. Output: Boolean True (not useful right now) """ - current_task_state=None - if len(self.task_states)>self.current_task_number: - current_task_state=self.task_states[self.current_task_number] + current_task_state = None + if len(self.task_states) > self.current_task_number: + current_task_state = self.task_states[self.current_task_number] - self.current_task_xml=self.task_xml[self.current_task_number] + self.current_task_xml = self.task_xml[self.current_task_number] - if self.current_task_number>0: - self.allow_reset=self.check_allow_reset() + if self.current_task_number > 0: + self.allow_reset = self.check_allow_reset() if self.allow_reset: - self.current_task_number=self.current_task_number-1 + self.current_task_number = self.current_task_number - 1 - current_task_type=self.get_tag_name(self.current_task_xml) + current_task_type = self.get_tag_name(self.current_task_xml) - children=self.child_modules() + children = self.child_modules() - self.current_task_descriptor=children['descriptors'][current_task_type](self.system) - etree_xml=etree.fromstring(self.current_task_xml) + self.current_task_descriptor = children['descriptors'][current_task_type](self.system) + etree_xml = etree.fromstring(self.current_task_xml) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) - if current_task_state is None and self.current_task_number==0: - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) + self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system) + if current_task_state is None and self.current_task_number == 0: + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - elif current_task_state is None and self.current_task_number>0: - last_response_data =self.get_last_response(self.current_task_number-1) + self.state = self.ASSESSING + elif current_task_state is None and self.current_task_number > 0: + last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] - current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + current_task_state = ( + '{"state": "' + str(self.ASSESSING) + '", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, + instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING + self.state = self.ASSESSING else: - if self.current_task_number>0 and not reset: - current_task_state=self.overwrite_state(current_task_state) - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + if self.current_task_number > 0 and not reset: + current_task_state = self.overwrite_state(current_task_state) + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, + instance_state=current_task_state) log.debug(current_task_state) return True @@ -235,13 +242,14 @@ class CombinedOpenEndedModule(XModule): Output: the allow_reset attribute of the current module. """ if not self.allow_reset: - if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_current_attributes(self.current_task_number) + if self.current_task_number > 0: + last_response_data = self.get_last_response(self.current_task_number - 1) + current_response_data = self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt'] last_response_data['score'] + or current_response_data['max_score_to_attempt'] < last_response_data['score']): + self.state = self.DONE + self.allow_reset = True return self.allow_reset @@ -251,18 +259,18 @@ class CombinedOpenEndedModule(XModule): Input: None Output: A dictionary that can be rendered into the combined open ended template. """ - task_html=self.get_html_base() + task_html = self.get_html_base() #set context variables and render template context = { - 'items': [{'content' : task_html}], + 'items': [{'content': task_html}], 'ajax_url': self.system.ajax_url, 'allow_reset': self.allow_reset, - 'state' : self.state, - 'task_count' : len(self.task_xml), - 'task_number' : self.current_task_number+1, - 'status' : self.get_status(), - } + 'state': self.state, + 'task_count': len(self.task_xml), + 'task_number': self.current_task_number + 1, + 'status': self.get_status(), + } return context @@ -272,7 +280,7 @@ class CombinedOpenEndedModule(XModule): Input: None Output: rendered html """ - context=self.get_context() + context = self.get_context() html = self.system.render_template('combined_open_ended.html', context) return html @@ -283,7 +291,7 @@ class CombinedOpenEndedModule(XModule): Input: None Output: HTML rendered directly via Mako """ - context=self.get_context() + context = self.get_context() html = render_to_string('combined_open_ended.html', context) return html @@ -304,11 +312,11 @@ class CombinedOpenEndedModule(XModule): Input: The number of the task. Output: The minimum and maximum scores needed to move on to the specified task. """ - task_xml=self.task_xml[task_number] - etree_xml=etree.fromstring(task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('max_score_to_attempt',self._max_score)) - return {'min_score_to_attempt' : min_score_to_attempt, 'max_score_to_attempt' : max_score_to_attempt} + task_xml = self.task_xml[task_number] + etree_xml = etree.fromstring(task_xml) + min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) + max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) + return {'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt} def get_last_response(self, task_number): """ @@ -316,49 +324,50 @@ class CombinedOpenEndedModule(XModule): Input: The number of the task. Output: A dictionary that contains information about the specified task. """ - last_response="" + last_response = "" task_state = self.task_states[task_number] - task_xml=self.task_xml[task_number] - task_type=self.get_tag_name(task_xml) + task_xml = self.task_xml[task_number] + task_type = self.get_tag_name(task_xml) - children=self.child_modules() + children = self.child_modules() - task_descriptor=children['descriptors'][task_type](self.system) - etree_xml=etree.fromstring(task_xml) + task_descriptor = children['descriptors'][task_type](self.system) + etree_xml = etree.fromstring(task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('max_score_to_attempt',self._max_score)) + min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) + max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) - task_parsed_xml=task_descriptor.definition_from_xml(etree_xml,self.system) - task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, self.static_data, instance_state=task_state) - last_response=task.latest_answer() + task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system) + task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, + self.static_data, instance_state=task_state) + last_response = task.latest_answer() last_score = task.latest_score() last_post_assessment = task.latest_post_assessment() - last_post_feedback="" - if task_type=="openended": + last_post_feedback = "" + if task_type == "openended": last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) - if isinstance(last_post_assessment,list): - eval_list=[] - for i in xrange(0,len(last_post_assessment)): + if isinstance(last_post_assessment, list): + eval_list = [] + for i in xrange(0, len(last_post_assessment)): eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) - last_post_evaluation="".join(eval_list) + last_post_evaluation = "".join(eval_list) else: last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation last_correctness = task.is_last_response_correct() max_score = task.max_score() state = task.state - last_response_dict={ - 'response' : last_response, - 'score' : last_score, - 'post_assessment' : last_post_assessment, - 'type' : task_type, - 'max_score' : max_score, - 'state' : state, - 'human_state' : task.HUMAN_NAMES[state], - 'correct' : last_correctness, - 'min_score_to_attempt' : min_score_to_attempt, - 'max_score_to_attempt' : max_score_to_attempt, + last_response_dict = { + 'response': last_response, + 'score': last_score, + 'post_assessment': last_post_assessment, + 'type': task_type, + 'max_score': max_score, + 'state': state, + 'human_state': task.HUMAN_NAMES[state], + 'correct': last_correctness, + 'min_score_to_attempt': min_score_to_attempt, + 'max_score_to_attempt': max_score_to_attempt, } return last_response_dict @@ -369,28 +378,28 @@ class CombinedOpenEndedModule(XModule): Input: None Output: boolean indicating whether or not the task state changed. """ - changed=False + changed = False if not self.allow_reset: self.task_states[self.current_task_number] = self.current_task.get_instance_state() - current_task_state=json.loads(self.task_states[self.current_task_number]) - if current_task_state['state']==self.DONE: - self.current_task_number+=1 - if self.current_task_number>=(len(self.task_xml)): - self.state=self.DONE - self.current_task_number=len(self.task_xml)-1 + current_task_state = json.loads(self.task_states[self.current_task_number]) + if current_task_state['state'] == self.DONE: + self.current_task_number += 1 + if self.current_task_number >= (len(self.task_xml)): + self.state = self.DONE + self.current_task_number = len(self.task_xml) - 1 else: - self.state=self.INITIAL - changed=True + self.state = self.INITIAL + changed = True self.setup_next_task() return changed - def update_task_states_ajax(self,return_html): + def update_task_states_ajax(self, return_html): """ Runs the update task states function for ajax calls. Currently the same as update_task_states Input: The html returned by the handle_ajax function of the child Output: New html that should be rendered """ - changed=self.update_task_states() + changed = self.update_task_states() if changed: #return_html=self.get_html() pass @@ -402,12 +411,12 @@ class CombinedOpenEndedModule(XModule): Input: AJAX get dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ - task_number=int(get['task_number']) + task_number = int(get['task_number']) self.update_task_states() - response_dict=self.get_last_response(task_number) - context = {'results' : response_dict['post_assessment'], 'task_number' : task_number+1} + response_dict = self.get_last_response(task_number) + context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1} html = render_to_string('combined_open_ended_results.html', context) - return {'html' : html, 'success' : True} + return {'html': html, 'success': True} def handle_ajax(self, dispatch, get): """ @@ -423,15 +432,15 @@ class CombinedOpenEndedModule(XModule): handlers = { 'next_problem': self.next_problem, 'reset': self.reset, - 'get_results' : self.get_results - } + 'get_results': self.get_results + } if dispatch not in handlers: - return_html = self.current_task.handle_ajax(dispatch,get, self.system) + return_html = self.current_task.handle_ajax(dispatch, get, self.system) return self.update_task_states_ajax(return_html) d = handlers[dispatch](get) - return json.dumps(d,cls=ComplexEncoder) + return json.dumps(d, cls=ComplexEncoder) def next_problem(self, get): """ @@ -440,7 +449,7 @@ class CombinedOpenEndedModule(XModule): Output: Dictionary to be rendered """ self.update_task_states() - return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} + return {'success': True, 'html': self.get_html_nonsystem(), 'allow_reset': self.allow_reset} def reset(self, get): """ @@ -457,17 +466,17 @@ class CombinedOpenEndedModule(XModule): 'success': False, 'error': 'Too many attempts.' } - self.state=self.INITIAL - self.allow_reset=False - for i in xrange(0,len(self.task_xml)): - self.current_task_number=i + self.state = self.INITIAL + self.allow_reset = False + for i in xrange(0, len(self.task_xml)): + self.current_task_number = i self.setup_next_task(reset=True) self.current_task.reset(self.system) - self.task_states[self.current_task_number]=self.current_task.get_instance_state() - self.current_task_number=0 - self.allow_reset=False + self.task_states[self.current_task_number] = self.current_task.get_instance_state() + self.current_task_number = 0 + self.allow_reset = False self.setup_next_task() - return {'success': True, 'html' : self.get_html_nonsystem()} + return {'success': True, 'html': self.get_html_nonsystem()} def get_instance_state(self): """ @@ -482,8 +491,8 @@ class CombinedOpenEndedModule(XModule): 'state': self.state, 'task_states': self.task_states, 'attempts': self.attempts, - 'ready_to_reset' : self.allow_reset, - } + 'ready_to_reset': self.allow_reset, + } return json.dumps(state) @@ -493,16 +502,17 @@ class CombinedOpenEndedModule(XModule): Input: None Output: The status html to be rendered """ - status=[] - for i in xrange(0,self.current_task_number+1): + status = [] + for i in xrange(0, self.current_task_number + 1): task_data = self.get_last_response(i) - task_data.update({'task_number' : i+1}) + task_data.update({'task_number': i + 1}) status.append(task_data) - context = {'status_list' : status} + context = {'status_list': status} status_html = self.system.render_template("combined_open_ended_status.html", context) return status_html + class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding combined open ended questions @@ -532,18 +542,18 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ expected_children = ['task', 'rubric', 'prompt'] for child in expected_children: - if len(xml_object.xpath(child)) == 0 : + if len(xml_object.xpath(child)) == 0: raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) def parse_task(k): """Assumes that xml_object has child k""" - return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] + return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))] def parse(k): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'task_xml': parse_task('task'), 'prompt' : parse('prompt'), 'rubric' : parse('rubric')} + return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')} def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5649cbbd2c..45d6501816 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -49,6 +49,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """ + def setup_response(self, system, location, definition, descriptor): """ Sets up the response type. @@ -65,8 +66,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE) #This is needed to attach feedback to specific responses later - self.submission_id=None - self.grader_id=None + self.submission_id = None + self.grader_id = None if oeparam is None: raise ValueError("No oeparam found in problem xml.") @@ -77,10 +78,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self._parse(oeparam, self.prompt, self.rubric, system) - if self.created=="True" and self.state == self.ASSESSING: - self.created="False" + if self.created == "True" and self.state == self.ASSESSING: + self.created = "False" self.send_to_grader(self.latest_answer(), system) - self.created="False" + self.created = "False" def _parse(self, oeparam, prompt, rubric, system): ''' @@ -94,8 +95,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload prompt_string = stringify_children(prompt) rubric_string = stringify_children(rubric) - self.prompt=prompt_string - self.rubric=rubric_string + self.prompt = prompt_string + self.rubric = rubric_string grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -113,13 +114,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') parsed_grader_payload.update({ - 'location' : system.location.url(), - 'course_id' : system.course_id, - 'prompt' : prompt_string, - 'rubric' : rubric_string, - 'initial_display' : self.initial_display, - 'answer' : self.answer, - }) + 'location': system.location.url(), + 'course_id': system.course_id, + 'prompt': prompt_string, + 'rubric': rubric_string, + 'initial_display': self.initial_display, + 'answer': self.answer, + }) updated_grader_payload = json.dumps(parsed_grader_payload) self.payload = {'grader_payload': updated_grader_payload} @@ -131,10 +132,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @param system: ModuleSystem @return: Success indicator """ - self.state=self.DONE - return {'success' : True} + self.state = self.DONE + return {'success': True} - def message_post(self,get, system): + def message_post(self, get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) Returns a boolean success/fail and an error message @@ -143,22 +144,23 @@ class OpenEndedModule(openendedchild.OpenEndedChild): event_info = dict() event_info['problem_id'] = system.location.url() event_info['student_id'] = system.anonymous_student_id - event_info['survey_responses']= get + event_info['survey_responses'] = get - survey_responses=event_info['survey_responses'] + survey_responses = event_info['survey_responses'] for tag in ['feedback', 'submission_id', 'grader_id', 'score']: if tag not in survey_responses: - return {'success' : False, 'msg' : "Could not find needed tag {0}".format(tag)} + return {'success': False, 'msg': "Could not find needed tag {0}".format(tag)} try: - submission_id=int(survey_responses['submission_id']) + submission_id = int(survey_responses['submission_id']) grader_id = int(survey_responses['grader_id']) feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) score = int(survey_responses['score']) except: - error_message=("Could not parse submission id, grader id, " - "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) + error_message = ("Could not parse submission id, grader id, " + "or feedback from message_post ajax call. Here is the message data: {0}".format( + survey_responses)) log.exception(error_message) - return {'success' : False, 'msg' : "There was an error saving your feedback. Please contact course staff."} + return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."} qinterface = system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -175,26 +177,26 @@ class OpenEndedModule(openendedchild.OpenEndedChild): student_info = {'anonymous_student_id': anonymous_student_id, 'submission_time': qtime, - } - contents= { - 'feedback' : feedback, - 'submission_id' : submission_id, - 'grader_id' : grader_id, + } + contents = { + 'feedback': feedback, + 'submission_id': submission_id, + 'grader_id': grader_id, 'score': score, - 'student_info' : json.dumps(student_info), - } + 'student_info': json.dumps(student_info), + } (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) #Convert error to a success value - success=True + success = True if error: - success=False + success = False - self.state=self.DONE + self.state = self.DONE - return {'success' : success, 'msg' : "Successfully submitted your feedback."} + return {'success': success, 'msg': "Successfully submitted your feedback."} def send_to_grader(self, submission, system): """ @@ -226,14 +228,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # Metadata related to the student submission revealed to the external grader student_info = {'anonymous_student_id': anonymous_student_id, 'submission_time': qtime, - } + } #Update contents with student response and student info contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, - 'max_score' : self.max_score(), - }) + 'max_score': self.max_score(), + }) # Submit request. When successful, 'msg' is the prior length of the queue (error, msg) = qinterface.send_to_queue(header=xheader, @@ -241,7 +243,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # State associated with the queueing request queuestate = {'key': queuekey, - 'time': qtime,} + 'time': qtime, } return True def _update_score(self, score_msg, queuekey, system): @@ -258,7 +260,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.record_latest_score(new_score_msg['score']) self.record_latest_post_assessment(score_msg) - self.state=self.POST_ASSESSMENT + self.state = self.POST_ASSESSMENT return True @@ -313,24 +315,24 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """ return priorities.get(elt[0], default_priority) - def encode_values(feedback_type,value): - feedback_type=str(feedback_type).encode('ascii', 'ignore') - if not isinstance(value,basestring): - value=str(value) - value=value.encode('ascii', 'ignore') - return feedback_type,value + def encode_values(feedback_type, value): + feedback_type = str(feedback_type).encode('ascii', 'ignore') + if not isinstance(value, basestring): + value = str(value) + value = value.encode('ascii', 'ignore') + return feedback_type, value def format_feedback(feedback_type, value): - feedback_type,value=encode_values(feedback_type,value) - feedback= """ + feedback_type, value = encode_values(feedback_type, value) + feedback = """

    {value}
    """.format(feedback_type=feedback_type, value=value) return feedback - def format_feedback_hidden(feedback_type , value): - feedback_type,value=encode_values(feedback_type,value) + def format_feedback_hidden(feedback_type, value): + feedback_type, value = encode_values(feedback_type, value) feedback = """ """.format(feedback_type=feedback_type, value=value) @@ -360,11 +362,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): else: feedback_list_part1 = format_feedback('errors', response_items['feedback']) - feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value) - for feedback_type,value in response_items.items() - if feedback_type in ['submission_id', 'grader_id']])) + feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value) + for feedback_type, value in response_items.items() + if feedback_type in ['submission_id', 'grader_id']])) - return u"\n".join([feedback_list_part1,feedback_list_part2]) + return u"\n".join([feedback_list_part1, feedback_list_part2]) def _format_feedback(self, response_items): """ @@ -378,13 +380,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not response_items['success']: return system.render_template("open_ended_error.html", - {'errors' : feedback}) + {'errors': feedback}) feedback_template = render_to_string("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, - }) + }) return feedback_template @@ -403,57 +405,57 @@ class OpenEndedModule(openendedchild.OpenEndedChild): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) """ - fail = {'valid' : False, 'score' : 0, 'feedback' : ''} + fail = {'valid': False, 'score': 0, 'feedback': ''} try: score_result = json.loads(score_msg) except (TypeError, ValueError): - error_message=("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) + error_message = ("External grader message should be a JSON-serialized dict." + " Received score_msg = {0}".format(score_msg)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail if not isinstance(score_result, dict): - error_message=("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) + error_message = ("External grader message should be a JSON-serialized dict." + " Received score_result = {0}".format(score_result)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: if tag not in score_result: - error_message=("External grader message is missing required tag: {0}" - .format(tag)) + error_message = ("External grader message is missing required tag: {0}" + .format(tag)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail - #This is to support peer grading + #This is to support peer grading if isinstance(score_result['score'], list): - feedback_items=[] - for i in xrange(0,len(score_result['score'])): - new_score_result={ - 'score' : score_result['score'][i], - 'feedback' : score_result['feedback'][i], - 'grader_type' : score_result['grader_type'], - 'success' : score_result['success'], - 'grader_id' : score_result['grader_id'][i], - 'submission_id' : score_result['submission_id'] - } + feedback_items = [] + for i in xrange(0, len(score_result['score'])): + new_score_result = { + 'score': score_result['score'][i], + 'feedback': score_result['feedback'][i], + 'grader_type': score_result['grader_type'], + 'success': score_result['success'], + 'grader_id': score_result['grader_id'][i], + 'submission_id': score_result['submission_id'] + } feedback_items.append(self._format_feedback(new_score_result)) if join_feedback: - feedback="".join(feedback_items) + feedback = "".join(feedback_items) else: - feedback=feedback_items + feedback = feedback_items score = int(median(score_result['score'])) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) - score=score_result['score'] + score = score_result['score'] - self.submission_id=score_result['submission_id'] - self.grader_id=score_result['grader_id'] + self.submission_id = score_result['submission_id'] + self.grader_id = score_result['grader_id'] - return {'valid' : True, 'score' : score, 'feedback' : feedback} + return {'valid': True, 'score': score, 'feedback': feedback} def latest_post_assessment(self, short_feedback=False, join_feedback=True): """ @@ -468,17 +470,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' if feedback_dict['valid']: - short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) + short_feedback = self._convert_longform_feedback_to_html( + json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' - def format_feedback_with_evaluation(self,feedback): + def format_feedback_with_evaluation(self, feedback): """ Renders a given html feedback into an evaluation template @param feedback: HTML feedback @return: Rendered html """ - context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} - html= render_to_string('open_ended_evaluation.html', context) + context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50} + html = render_to_string('open_ended_evaluation.html', context) return html def handle_ajax(self, dispatch, get, system): @@ -494,10 +497,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): handlers = { 'save_answer': self.save_answer, 'score_update': self.update_score, - 'save_post_assessment' : self.message_post, - 'skip_post_assessment' : self.skip_post_assessment, - 'check_for_score' : self.check_for_score, - } + 'save_post_assessment': self.message_post, + 'skip_post_assessment': self.skip_post_assessment, + 'check_for_score': self.check_for_score, + } if dispatch not in handlers: return 'Error' @@ -508,7 +511,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): d.update({ 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), - }) + }) return json.dumps(d, cls=ComplexEncoder) def check_for_score(self, get, system): @@ -519,7 +522,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @return: Returns the current state """ state = self.state - return {'state' : state} + return {'state': state} def save_answer(self, get, system): """ @@ -545,7 +548,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.send_to_grader(get['student_answer'], system) self.change_state(self.ASSESSING) - return {'success': True,} + return {'success': True, } def update_score(self, get, system): """ @@ -571,11 +574,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): latest = self.latest_answer() previous_answer = latest if latest is not None else self.initial_display post_assessment = self.latest_post_assessment() - score= self.latest_score() + score = self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' else: - post_assessment="" - correct="" + post_assessment = "" + correct = "" previous_answer = self.initial_display context = { @@ -583,17 +586,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'previous_answer': previous_answer, 'state': self.state, 'allow_reset': self._allow_reset(), - 'rows' : 30, - 'cols' : 80, - 'id' : 'open_ended', - 'msg' : post_assessment, - 'child_type' : 'openended', - 'correct' : correct, - } + 'rows': 30, + 'cols': 80, + 'id': 'open_ended', + 'msg': post_assessment, + 'child_type': 'openended', + 'correct': correct, + } log.debug(context) html = system.render_template('open_ended.html', context) return html + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding open ended response questions to courses @@ -627,7 +631,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'oeparam': parse('openendedparam'),} + return {'oeparam': parse('openendedparam'), } def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index ce1b15074f..aa83a35c9d 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -63,11 +63,11 @@ class OpenEndedChild(): DONE = 'done' #This is used to tell students where they are at in the module - HUMAN_NAMES={ - 'initial' : 'Started', - 'assessing' : 'Being scored', - 'post_assessment' : 'Scoring finished', - 'done' : 'Problem complete', + HUMAN_NAMES = { + 'initial': 'Started', + 'assessing': 'Being scored', + 'post_assessment': 'Scoring finished', + 'done': 'Problem complete', } def __init__(self, system, location, definition, descriptor, static_data, @@ -84,7 +84,7 @@ class OpenEndedChild(): # Scores are on scale from 0 to max_score self.history = instance_state.get('history', []) - self.state = instance_state.get('state', 'initial') + self.state = instance_state.get('state', self.INITIAL) self.created = instance_state.get('created', "False") @@ -171,8 +171,8 @@ class OpenEndedChild(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, - 'created' : "False", - } + 'created': "False", + } return json.dumps(state) def _allow_reset(self): @@ -244,8 +244,8 @@ class OpenEndedChild(): @param score: Numeric score. @return: Boolean correct. """ - correct=False - if(isinstance(score,(int, long, float, complex))): + correct = False + if(isinstance(score, (int, long, float, complex))): score_ratio = int(score) / float(self.max_score()) correct = (score_ratio >= 0.66) return correct @@ -255,7 +255,7 @@ class OpenEndedChild(): Checks to see if the last response in the module is correct. @return: 'correct' if correct, otherwise 'incorrect' """ - score=self.get_score()['score'] + score = self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' return correctness diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 88632a38d0..870f3ea169 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -40,6 +40,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): """ + def setup_response(self, system, location, definition, descriptor): """ Sets up the module @@ -76,7 +77,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), - 'child_type' : 'selfassessment', + 'child_type': 'selfassessment', } html = system.render_template('self_assessment_prompt.html', context) @@ -112,7 +113,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) - def get_rubric_html(self,system): + def get_rubric_html(self, system): """ Return the appropriate version of the rubric, based on the state. """ @@ -121,8 +122,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): # we'll render it context = {'rubric': self.rubric, - 'max_score' : self._max_score, - } + 'max_score': self._max_score, + } if self.state == self.ASSESSING: context['read_only'] = False @@ -133,7 +134,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return system.render_template('self_assessment_rubric.html', context) - def get_hint_html(self,system): + def get_hint_html(self, system): """ Return the appropriate version of the hint view, based on state. """ @@ -201,7 +202,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return { 'success': True, 'rubric_html': self.get_rubric_html(system) - } + } def save_assessment(self, get, system): """ @@ -228,7 +229,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): self.record_latest_score(score) - d = {'success': True,} + d = {'success': True, } if score == self.max_score(): self.change_state(self.DONE) @@ -264,7 +265,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'allow_reset': self._allow_reset()} - class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses @@ -302,7 +302,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): return {'submitmessage': parse('submitmessage'), 'hintprompt': parse('hintprompt'), - } + } def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' From 1dca370a7f9b0f328ebf1da826f1304aebd60696 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 09:50:43 -0500 Subject: [PATCH 204/541] Better initial documentation of combined open ended --- .../xmodule/xmodule/combined_open_ended_module.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 358a3b6995..17355e9ce2 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -39,6 +39,19 @@ class CombinedOpenEndedModule(XModule): """ This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). It transitions between problems, and support arbitrary ordering. + Each combined open ended module contains one or multiple "child" modules. + Child modules track their own state, and can transition between states. They also implement get_html and + handle_ajax. + The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess + ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem) + ajax actions implemented by all children are: + 'save_answer' -- Saves the student answer + 'save_assessment' -- Saves the student assessment (or external grader assessment) + 'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc) + ajax actions implemented by combined open ended module are: + 'reset' -- resets the whole combined open ended module and returns to the first child module + 'next_problem' -- moves to the next child module + 'get_results' -- gets results from a given child module """ STATE_VERSION = 1 @@ -163,7 +176,7 @@ class CombinedOpenEndedModule(XModule): def child_modules(self): """ - Returns the functions associated with the child modules in a dictionary. This makes writing functions + Returns the constructors associated with the child modules in a dictionary. This makes writing functions simpler (saves code duplication) Input: None Output: A dictionary of dictionaries containing the descriptor functions and module functions From 5d40d22136cd76acb999669446621138e765c14a Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 11 Jan 2013 09:52:46 -0500 Subject: [PATCH 205/541] revised rejected state error message content, markup, and styling on registration's right hand side status area --- .../sass/multicourse/_testcenter-register.scss | 10 +++++++--- lms/templates/test_center_register.html | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lms/static/sass/multicourse/_testcenter-register.scss b/lms/static/sass/multicourse/_testcenter-register.scss index b2b860b60b..974c108e0b 100644 --- a/lms/static/sass/multicourse/_testcenter-register.scss +++ b/lms/static/sass/multicourse/_testcenter-register.scss @@ -377,7 +377,7 @@ $red: rgb(178, 6, 16); } } - .details, .item { + .details, .item, .instructions { @include transition(opacity, 0.10s, ease-in-out); font-size: 13px; opacity: 0.65; @@ -432,6 +432,10 @@ $red: rgb(178, 6, 16); color: red; content: "rejected"; } + + .call-link { + font-weight: bold; + } } &.status-initial { @@ -448,14 +452,14 @@ $red: rgb(178, 6, 16); &:hover { - .details, .item { + .details, .item, .instructions { opacity: 1.0; } } } // sub menus - .accommodations-list { + .accommodations-list, .error-list { list-style: none; margin: ($baseline/2) 0; padding: 0; diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index ea63b8261a..6e042d5dc2 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -8,7 +8,7 @@ <%namespace name='static' file='static_content.html'/> -<%block name="title">Pearson VUE Test Center Proctoring - Sign Up +<%block name="title">Pearson VUE Test Center Proctoring - Registration <%block name="js_extra"> + +
    -
    -
    -

    Organization

    -
    -

    What is edX?

    -

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

    -

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

    -
    +
    +

    edX Basics

    -

    Will edX be adding additional X Universities?

    -

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

    -

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

    +

    How do I sign up to take a class?

    +
    +

    Simply create an edX account (it's free) and then register for the course of your choice (also free). Follow the prompts on the edX website.

    +
    +
    +
    +

    What does it cost to take a class? Is this really free?

    +
    +

    EdX courses are free for everyone. All you need is an Internet connection.

    +
    +
    +
    +

    What happens after I sign up for a course?

    +
    +

    You will receive an activation email. Follow the prompts in that email to activate your account. You will need to log in each time you access your course(s). Once the course begins, it’s time to hit the virtual books. You can access the lectures, homework, tutorials, etc., for each week, one week at a time.

    +
    +
    +
    +

    Who can take an edX course?

    +
    +

    You, your mom, your little brother, your grandfather -- anyone with Internet access can take an edX course. Free.

    +
    +
    +
    +

    Are the courses only offered in English?

    +
    +

    Some edX courses include a translation of the lecture in the text bar to the right of the video. Some have the specific option of requesting a course in other languages. Please check your course to determine foreign language options.

    +
    +
    +
    +

    When will there be more courses on other subjects?

    +
    +

    We are continually reviewing and creating courses to add to the edX platform. Please check the website for future course announcements. You can also "friend" edX on Facebook – you’ll receive updates and announcements.

    +
    +
    +
    +

    How can I help edX?

    +
    +

    You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.

    +
    +
    +
    +

    When does my course start and/or finish?

    +
    +

    You can find the start and stop dates for each course on each course description page.

    +
    +
    +
    +

    Is there a walk-through of a sample course session?

    +
    +

    There are video introductions for every course that will give you a good sense of how the course works and what to expect.

    +
    +
    +
    +

    I don't have the prerequisites for a course that I am interested in. Can I still take the course?

    +
    +

    We do not check students for prerequisites, so you are allowed to attempt the course. However, if you do not know prerequisite subjects before taking a class, you will have to learn the prerequisite material on your own over the semester, which can be an extremely difficult task.

    +
    +
    +
    +

    What happens if I have to quit a course, are there any penalties, will I be able to take another course in the future?

    +
    +

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    +
    -
    -

    Students

    +
    +

    The Classes

    -

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

    -

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

    +

    How much work will I have to do to pass my course?

    +
    +

    The projected hours of study required for each course are described on the specific course description page.

    +
    -

    Will certificates be awarded?

    -

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

    +

    What should I do before I take a course (prerequisites)?

    +
    +

    Each course is different – some have prerequisites, and some don’t. Take a look at your specific course’s recommended prerequisites. If you do not have a particular prerequisite, you may still take the course.

    +
    -

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

    -

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

    +

    What books should I read? (I am interested in reading materials before the class starts).

    +
    +

    Take a look at the specific course prerequisites. All required academic materials will be provided during the course, within the browser. Some of the course descriptions may list additional resources. For supplemental reading material before or during the course, you can post a question on the course’s Discussion Forum to ask your online coursemates for suggestions.

    +
    -

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

    -

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

    +

    Can I download the book for my course?

    +
    +

    EdX book content may only be viewed within the browser, and downloading it violates copyright laws. If you need or want a hard copy of the book, we recommend that you purchase a copy.

    +
    -

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

    -

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

    +

    Can I take more than one course at a time?

    +
    +

    You may take multiple edX courses, however we recommend checking the requirements on each course description page to determine your available study hours and the demands of the intended courses.

    +
    -

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

    -

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

    +

    How do I log in to take an edX class?

    +
    +

    Once you sign up for a course and activate your account, click on the "Log In" button on the edx.org home page. You will need to type in your email address and edX password each time you log in.

    +
    -

    How may I apply to study with edX?

    -

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

    +

    What time is the class?

    +
    +

    EdX classes take place at your convenience. Prefer to sleep in and study late? No worries. Videos and problem sets are available 24 hours a day, which means you can watch video and complete work whenever you have spare time. You simply log in to your course via the Internet and work through the course material, one week at a time.

    +
    -

    How may another university participate in edX?

    -

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

    +

    If I miss a week, how does this affect my grade?

    +
    +

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    +
    +
    +
    +

    How can I meet/find other students?

    +
    +

    All edX courses have Discussion Forums where you can chat with and help each other within the framework of the Honor Code.

    +
    +
    +
    +

    How can I talk to professors, fellows and teaching assistants?

    +
    +

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    +
    -
    -

    Technology Platform

    +
    +

    Getting Help

    -

    What technology will edX use?

    -

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

    -

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

    +

    Can I re-take a course?

    +
    +

    Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.

    +
    -

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

    -

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

    +

    Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?

    +
    +

    Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.

    +
    +
    +
    +

    Is there an exam at the end?

    +
    +

    Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.

    +
    +
    +
    +

    Will the same courses be offered again in the future?

    +
    +

    Existing edX courses will be re-offered, and more courses added.

    +
    +
    +
    +

    Will I get a certificate for taking an edX course?

    +
    +

    Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.

    +
    +
    +
    +

    How are edX certificates delivered?

    +
    +

    EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.

    +
    +
    +
    +

    What is the difference between a proctored certificate and an honor code certificate?

    +
    +

    A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed the edX honor code .

    +
    +
    +
    +

    Yes. The requirements for both certificates can be independently satisfied.

    +
    +

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    +
    +
    +
    +

    Will my grade be shown on my certificate?

    +
    +

    No. Grades are not displayed on either honor code or proctored certificates.

    +
    +
    +
    +

    How can I talk to professors, fellows and teaching assistants?

    +
    +

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    +
    +
    +
    +

    The only certificates distributed with grades by edX were for the initial prototype course.

    +
    +

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    +
    +
    +
    +

    Will my university accept my edX coursework for credit?

    +
    +

    Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.

    +
    +
    +
    +

    I lost my edX certificate – can you resend it to me?

    +
    +

    Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.

    +
    +
    +
    + +
    +

    edX & Open Source

    +
    +

    What’s open source?

    +
    +

    Open source is a philosophy that generally refers to making software freely available for use or modification as users see fit. In exchange for use of the software, users generally add their contributions to the software, making it a public collaboration. The edX platform will be made available as open source code in order to allow world talent to improve and share it on an ongoing basis.

    +
    +
    +
    +

    When/how can I get the open-source platform technology?

    +
    +

    We are still building the edX technology platform and will be making announcements in the future about its availability.

    +
    +
    +
    + +
    +

    Other Help Questions - Account Questions

    +
    +

    My username is taken.

    +
    +

    Now’s your chance to be creative: please try a different, more unique username – for example, try adding a random number to the end.

    +
    +
    +
    +

    Why does my password show on my course login page?

    +
    +

    Oops! This may be because of the way you created your account. For example, you may have mistakenly typed your password into the login box.

    +
    +
    +
    +

    I am having login problems (password/email unrecognized).

    +
    +

    Please check your browser’s settings to make sure that you have the current version of Firefox or Chrome, and then try logging in again. If you find access impossible, you may simply create a new account using an alternate email address – the old, unused account will disappear later.

    +
    +
    +
    +

    I did not receive an activation email.

    +
    +

    If you did not receive an activation email it may be because:

    +
      +
    • There was a typo in your email address.
    • +
    • Your spam filter may have caught the activation email. Please check your spam folder.
    • +
    • You may be using an older browser. We recommend downloading the current version of Firefox or Chrome.
    • +
    • JavaScript is disabled in your browser. Please check your browser settings and confirm that JavaScript is enabled.
    • +
    +

    If you continue to have problems activating your account, we recommend that you try creating a new account. There is no need to do anything about the old account. If it is not activated through the link in the email, it will disappear later.

    +
    +
    +
    +

    Can I delete my account?

    +
    +

    There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.

    +
    +
    +
    +

    I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.

    +
    +

    Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.

    +
    +
    From 4ffa00b4009d9014a94a92bff58b16a5f56704ad Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 15:00:13 -0500 Subject: [PATCH 223/541] Add in notifications, peer grading tab, tab image --- lms/djangoapps/courseware/tabs.py | 34 ++++++++++++++++++- .../peer_grading_service.py | 6 ++++ .../courseware/course_navigation.html | 7 +++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 45b4e1821c..1f13db0064 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -11,6 +11,7 @@ actually generates the CourseTab. from collections import namedtuple import logging +import json from django.conf import settings from django.core.urlresolvers import reverse @@ -20,6 +21,9 @@ from fs.errors import ResourceNotFoundError from courseware.access import has_access from static_replace import replace_urls +from open_ended_grading.peer_grading_service import PeerGradingService +from student.models import unique_id_for_user + log = logging.getLogger(__name__) class InvalidTabsException(Exception): @@ -28,7 +32,10 @@ class InvalidTabsException(Exception): """ pass -CourseTab = namedtuple('CourseTab', 'name link is_active') +CourseTabBase = namedtuple('CourseTab', 'name link is_active has_img img') + +def CourseTab(name, link, is_active, has_img=False, img=""): + return CourseTabBase(name, link, is_active, has_img, img) # encapsulate implementation for a tab: # - a validation function: takes the config dict and raises @@ -104,6 +111,30 @@ def _staff_grading(tab, user, course, active_page): return [CourseTab('Staff grading', link, active_page == "staff_grading")] return [] +def _peer_grading(tab, user, course, active_page): + link = reverse('peer_grading', args=[course.id]) + peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) + pending_grading=False + try: + notifications = json.loads(peer_gs.get_notifications(course.id,unique_id_for_user(user))) + log.debug(notifications) + + if notifications['success']: + if notifications['student_needs_to_peer_grade']: + pending_grading=True + except: + #Non catastrophic error, so no real action + log.info("Problem with getting notifications from peer grading service.") + + if pending_grading: + tab_name = "Peer grading (Pending)" + img_path = "/static/images/unanswered-icon.png" + else: + tab_name = "Peer Grading" + img_path= "" + + tab = [CourseTab(tab_name, link, active_page == "peer_grading", True, img_path)] + return tab #### Validators @@ -141,6 +172,7 @@ VALID_TAB_TYPES = { 'progress': TabImpl(need_name, _progress), 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), 'staff_grading': TabImpl(null_validator, _staff_grading), + 'peer_grading': TabImpl(null_validator, _peer_grading), } diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 9ef0383fb5..8e0f8cbbaa 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -79,6 +79,7 @@ class PeerGradingService(GradingService): self.show_calibration_essay_url = self.url + '/show_calibration_essay/' self.save_calibration_essay_url = self.url + '/save_calibration_essay/' self.get_problem_list_url = self.url + '/get_problem_list/' + self.get_notifications_url = self.url + '/get_notifications/' def get_next_submission(self, problem_location, grader_id): response = self.get(self.get_next_submission_url, @@ -116,6 +117,11 @@ class PeerGradingService(GradingService): response = self.get(self.get_problem_list_url, params) return response + def get_notifications(self, course_id, grader_id): + params = {'course_id': course_id, 'student_id': grader_id} + response = self.get(self.get_notifications_url, params) + return response + _service = None def peer_grading_service(): diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 5ae69908fb..ec9004c0e8 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -18,7 +18,12 @@ def url_class(is_active):
      % for tab in get_course_tabs(user, course, active_page):
    1. - ${tab.name | h} + + ${tab.name | h} + % if tab.has_img == True: + + %endif +
    2. % endfor <%block name="extratabs" /> From cc56e4763383fdfd1a1590335a074ab43656ec1d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 15:12:54 -0500 Subject: [PATCH 224/541] Fix active page issue --- lms/djangoapps/courseware/tabs.py | 8 ++++---- lms/templates/peer_grading/peer_grading.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 1f13db0064..139a527a42 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -127,10 +127,10 @@ def _peer_grading(tab, user, course, active_page): log.info("Problem with getting notifications from peer grading service.") if pending_grading: - tab_name = "Peer grading (Pending)" - img_path = "/static/images/unanswered-icon.png" + tab_name = "Peer grading" + img_path = "/static/images/slider-handle.png" else: - tab_name = "Peer Grading" + tab_name = "Peer grading" img_path= "" tab = [CourseTab(tab_name, link, active_page == "peer_grading", True, img_path)] @@ -171,8 +171,8 @@ VALID_TAB_TYPES = { 'textbooks': TabImpl(null_validator, _textbooks), 'progress': TabImpl(need_name, _progress), 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), - 'staff_grading': TabImpl(null_validator, _staff_grading), 'peer_grading': TabImpl(null_validator, _peer_grading), + 'staff_grading': TabImpl(null_validator, _staff_grading), } diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 484bb94182..2eb33cae05 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -8,7 +8,7 @@ <%block name="title">${course.number} Peer Grading -<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> +<%include file="/courseware/course_navigation.html" args="active_page='peer_grading'" /> <%block name="js_extra"> <%static:js group='peer_grading'/> From 4353c4ab056680067809b96804e6b052bfe7dbba Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 15:16:06 -0500 Subject: [PATCH 225/541] Clean up some logic --- lms/djangoapps/courseware/tabs.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 139a527a42..18d6612cc6 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -115,6 +115,8 @@ def _peer_grading(tab, user, course, active_page): link = reverse('peer_grading', args=[course.id]) peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) pending_grading=False + tab_name = "Peer grading" + img_path= "" try: notifications = json.loads(peer_gs.get_notifications(course.id,unique_id_for_user(user))) log.debug(notifications) @@ -127,11 +129,7 @@ def _peer_grading(tab, user, course, active_page): log.info("Problem with getting notifications from peer grading service.") if pending_grading: - tab_name = "Peer grading" img_path = "/static/images/slider-handle.png" - else: - tab_name = "Peer grading" - img_path= "" tab = [CourseTab(tab_name, link, active_page == "peer_grading", True, img_path)] return tab From 96da4ac55ede671a4f6147b60598f463cb35b943 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 14 Jan 2013 16:27:28 -0500 Subject: [PATCH 226/541] Add some log statements. Add missing fields to _make_tc_user, and make _make_tc_registration independent of course configuration (for testing) --- .../commands/pearson_make_tc_registration.py | 14 ++++-- .../commands/pearson_make_tc_user.py | 47 ++++++++++++++++++ common/djangoapps/student/models.py | 4 +- common/djangoapps/student/views.py | 49 ++++++++++--------- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py index 21a2da442a..5536844cab 100644 --- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py +++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py @@ -1,4 +1,5 @@ from optparse import make_option +from time import strftime from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError @@ -6,6 +7,7 @@ from django.core.management.base import BaseCommand, CommandError from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_testcenter_registration from student.views import course_from_id from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.exceptions import ItemNotFoundError class Command(BaseCommand): option_list = BaseCommand.option_list + ( @@ -91,14 +93,14 @@ class Command(BaseCommand): raise CommandError("User {%s} does not exist".format(student)) # check to see if a course_id was specified, and use information from that: - course = course_from_id(course_id) - if course is not None: + try: + course = course_from_id(course_id) if 'ignore_registration_dates' in our_options: examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')] exam = examlist[0] if len(examlist) > 0 else None else: exam = course.current_test_center_exam - else: + except ItemNotFoundError: # otherwise use explicit values (so we don't have to define a course): exam_name = "Dummy Placeholder Name" exam_info = { 'Exam_Series_Code': our_options['exam_series_code'], @@ -106,6 +108,10 @@ class Command(BaseCommand): 'Last_Eligible_Appointment_Date' : our_options['eligibility_appointment_date_last'], } exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info) + # update option values for date_first and date_last to use YYYY-MM-DD format + # instead of YYYY-MM-DDTHH:MM + our_options['eligibility_appointment_date_first'] = strftime("%Y-%m-%d", exam.first_eligible_appointment_date) + our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) if exam is None: raise CommandError("Exam for course_id {%s} does not exist".format(course_id)) @@ -167,6 +173,8 @@ class Command(BaseCommand): # override internal values: change_internal = False + if 'exam_series_code' in our_options: + exam_code = our_options['exam_series_code'] registration = get_testcenter_registration(student, course_id, exam_code)[0] for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id']: if internal_field in our_options: diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_user.py b/common/djangoapps/student/management/commands/pearson_make_tc_user.py index 2426b7f2ce..57ee78e391 100644 --- a/common/djangoapps/student/management/commands/pearson_make_tc_user.py +++ b/common/djangoapps/student/management/commands/pearson_make_tc_user.py @@ -13,16 +13,41 @@ class Command(BaseCommand): action='store', dest='first_name', ), + make_option( + '--middle_name', + action='store', + dest='middle_name', + ), make_option( '--last_name', action='store', dest='last_name', ), + make_option( + '--suffix', + action='store', + dest='suffix', + ), + make_option( + '--salutation', + action='store', + dest='salutation', + ), make_option( '--address_1', action='store', dest='address_1', ), + make_option( + '--address_2', + action='store', + dest='address_2', + ), + make_option( + '--address_3', + action='store', + dest='address_3', + ), make_option( '--city', action='store', @@ -51,12 +76,34 @@ class Command(BaseCommand): dest='phone', help='Pretty free-form (parens, spaces, dashes), but no country code' ), + make_option( + '--extension', + action='store', + dest='extension', + ), make_option( '--phone_country_code', action='store', dest='phone_country_code', help='Phone country code, just "1" for the USA' ), + make_option( + '--fax', + action='store', + dest='fax', + help='Pretty free-form (parens, spaces, dashes), but no country code' + ), + make_option( + '--fax_country_code', + action='store', + dest='fax_country_code', + help='Fax country code, just "1" for the USA' + ), + make_option( + '--company_name', + action='store', + dest='company_name', + ), # internal values: make_option( '--client_candidate_id', diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index da6f260a7d..d15afc5d83 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -223,8 +223,6 @@ class TestCenterUser(models.Model): return self.user.email def needs_update(self, dict): -# needs_updating = any([__getattribute__(fieldname) != dict[fieldname] -# for fieldname in TestCenterUser.user_provided_fields()]) for fieldname in TestCenterUser.user_provided_fields(): if fieldname in dict and self.__getattribute__(fieldname) != dict[fieldname]: return True @@ -275,6 +273,7 @@ class TestCenterUserForm(ModelForm): # create additional values here: new_user.user_updated_at = datetime.utcnow() new_user.save() + log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.username)) # add validation: @@ -534,6 +533,7 @@ class TestCenterRegistrationForm(ModelForm): # create additional values here: registration.user_updated_at = datetime.utcnow() registration.save() + log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.username, registration.course_id, registration.exam_series_code)) # TODO: add validation code for values added to accommodation_code field. diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 72c7798eb2..b41de103ca 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -694,10 +694,13 @@ def create_test_registration(request, post_override=None): registrations = get_testcenter_registration(user, course_id, exam_code) if len(registrations) > 0: registration = registrations[0] - # TODO: check to see if registration changed. Should check appointment dates too... - # And later should check changes in accommodation_code. - # But at the moment, we don't expect anything to cause this to change - # because of the registration form. + # NOTE: we do not bother to check here to see if the registration has changed, + # because at the moment there is no way for a user to change anything about their + # registration. They only provide an optional accommodation request once, and + # cannot make changes to it thereafter. + # It is possible that the exam_info content has been changed, such as the + # scheduled exam dates, but those kinds of changes should not be handled through + # this registration screen. else: accommodation_request = post_vars.get('accommodation_request','') @@ -720,27 +723,25 @@ def create_test_registration(request, post_override=None): # only do the following if there is accommodation text to send, # and a destination to which to send it. # TODO: still need to create the accommodation email templates - if 'accommodation_request' in post_vars and settings.MITX_FEATURES.get('ACCOMMODATION_EMAIL'): - d = {'accommodation_request': post_vars['accommodation_request'] } - - # composes accommodation email - subject = render_to_string('emails/accommodation_email_subject.txt', d) - # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) - message = render_to_string('emails/accommodation_email.txt', d) +# if 'accommodation_request' in post_vars and 'TESTCENTER_ACCOMMODATION_REQUEST_EMAIL' in settings: +# d = {'accommodation_request': post_vars['accommodation_request'] } +# +# # composes accommodation email +# subject = render_to_string('emails/accommodation_email_subject.txt', d) +# # Email subject *must not* contain newlines +# subject = ''.join(subject.splitlines()) +# message = render_to_string('emails/accommodation_email.txt', d) +# +# try: +# dest_addr = settings['TESTCENTER_ACCOMMODATION_REQUEST_EMAIL'] +# from_addr = user.email +# send_mail(subject, message, from_addr, [dest_addr], fail_silently=False) +# except: +# log.exception(sys.exc_info()) +# response_data = {'success': False} +# response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ] +# return HttpResponse(json.dumps(response_data), mimetype="application/json") - # skip if destination email address is not specified - try: - dest_addr = settings.MITX_FEATURES['ACCOMMODATION_EMAIL'] - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False) - except: - log.exception(sys.exc_info()) - response_data = {'success': False} - response_data['non_field_errors'] = [ 'Could not send accommodation e-mail.', ] - return HttpResponse(json.dumps(response_data), mimetype="application/json") - - # TODO: enable appropriate stat - # statsd.increment("common.student.account_created") js = {'success': True} return HttpResponse(json.dumps(js), mimetype="application/json") From 4e83f748b745e03ed11de37ce33a279669ddf89d Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 14 Jan 2013 17:31:05 -0500 Subject: [PATCH 227/541] pearson registration - revised contact calls to action and order in status section, edited flash message copy/styling, added in strawman state for disabled form/closed registration state of form, added in strawman state for demographics rejected flash message, revised accomodations text labe and isntructions --- .../multicourse/_testcenter-register.scss | 59 ++++++++++++-- lms/static/sass/shared/_forms.scss | 3 +- lms/templates/test_center_register.html | 77 ++++++++++++------- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/lms/static/sass/multicourse/_testcenter-register.scss b/lms/static/sass/multicourse/_testcenter-register.scss index 974c108e0b..e5e168f6b7 100644 --- a/lms/static/sass/multicourse/_testcenter-register.scss +++ b/lms/static/sass/multicourse/_testcenter-register.scss @@ -10,6 +10,11 @@ $red: rgb(178, 6, 16); @include clearfix; padding: 60px 0px 120px; + // reset - horrible, but necessary + p, a, h1, h2, h3, h4, h5, h6 { + font-family: $sans-serif !important; + } + // basic layout .introduction { width: flex-grid(12); @@ -74,7 +79,6 @@ $red: rgb(178, 6, 16); .instructions, .note { margin: 0; padding: ($baseline*1.5) ($baseline*1.5) 0 ($baseline*1.5); - font-family: $sans-serif; font-size: 14px; color: tint($base-font-color, 20%); @@ -106,6 +110,11 @@ $red: rgb(178, 6, 16); letter-spacing: 0; padding: ($baseline*0.75) $baseline; text-align: center; + + + &:disabled { + opacity: 0.3; + } } .action-primary { @@ -181,7 +190,6 @@ $red: rgb(178, 6, 16); border: 1px solid #C8C8C8; padding: $baseline ($baseline*0.75); background: #FAFAFA; - font-family: $sans-serif; } } @@ -278,6 +286,25 @@ $red: rgb(178, 6, 16); } } } + + &.disabled { + + > .instructions { + display: none; + } + + .field { + opacity: 0.5; + + .label { + cursor: default; + } + } + + .form-actions { + display: none; + } + } } // form - specifics @@ -287,13 +314,19 @@ $red: rgb(178, 6, 16); &.is-shown { display: block; } + + &.disabled { + + fieldset { + opacity: 0.5; + } + } } .form-fields-secondary-visibility { display: block; margin: 0; padding: $baseline ($baseline*1.5) 0 ($baseline*1.5); - font-family: $sans-serif; font-size: 13px; &.is-hidden { @@ -321,7 +354,6 @@ $red: rgb(178, 6, 16); margin: 0 0 ($baseline/4) 0; padding: 0; font-size: 13px; - font-family: $sans-serif; } .label, .value { @@ -407,6 +439,12 @@ $red: rgb(178, 6, 16); color: green; content: "processed"; } + + &.status-registration { + .exam-link { + font-weight: 600 !important; + } + } } &.status-pending { @@ -473,6 +511,10 @@ $red: rgb(178, 6, 16); } // actions + .contact-link { + font-weight: 600; + } + .actions { @include box-shadow(inset 0 1px 1px 0px rgba(0,0,0,0.2)); border-top: 1px solid tint(rgb(0,0,0), 90%); @@ -565,6 +607,7 @@ $red: rgb(178, 6, 16); .value { color: rgb(0,0,0); + font-weight: 600; } } @@ -598,7 +641,6 @@ $red: rgb(178, 6, 16); } .message-copy { - font-family: $sans-serif; font-size: 14px; } @@ -614,15 +656,18 @@ $red: rgb(178, 6, 16); font-size: 14px; .message-title, .message-copy { - font-family: $sans-serif; - font-size: 14px; } .message-title { font-weight: bold; + font-size: 16px; margin: 0 0 ($baseline/4) 0; } + .message-copy { + font-size: 14px; + } + .contact-button { @include button(simple, $blue); } diff --git a/lms/static/sass/shared/_forms.scss b/lms/static/sass/shared/_forms.scss index d6a5f482e3..79d476f420 100644 --- a/lms/static/sass/shared/_forms.scss +++ b/lms/static/sass/shared/_forms.scss @@ -13,7 +13,8 @@ label { textarea, input[type="text"], input[type="email"], -input[type="password"] { +input[type="password"], +input[type="tel"] { background: rgb(250,250,250); border: 1px solid rgb(200,200,200); @include border-radius(3px); diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index a0bba90445..00f0c6b17a 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -14,12 +14,15 @@ From 643ac69a57af84733318fd06675a3f1126661ff2 Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 16:56:31 +0200 Subject: [PATCH 239/541] Separating JS from the html for GST. --- common/static/js/graphical_slider_tool/main.js | 1 + lms/templates/graphical_slider_tool.html | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js new file mode 100644 index 0000000000..bb624d5888 --- /dev/null +++ b/common/static/js/graphical_slider_tool/main.js @@ -0,0 +1 @@ +alert('Hello, world!'); diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index fc5052893a..97fca83ff4 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -14,9 +14,9 @@ ${gst_html}
    - From 0913e9ef69c6f12f5d87195c54d683de040bc82c Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 17:16:57 +0200 Subject: [PATCH 240/541] Work on GST. --- .../js/src/graphical_slider_tool/module.js | 15 ++++ .../js/graphical_slider_tool/gst_module.js | 15 ++++ .../static/js/graphical_slider_tool/main.js | 76 ++++++++++++++++++- lms/envs/common.py | 1 + lms/templates/graphical_slider_tool.html | 26 ++----- 5 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js create mode 100644 common/static/js/graphical_slider_tool/gst_module.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/static/js/graphical_slider_tool/gst_module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js index bb624d5888..da36d9c9d6 100644 --- a/common/static/js/graphical_slider_tool/main.js +++ b/common/static/js/graphical_slider_tool/main.js @@ -1 +1,75 @@ -alert('Hello, world!'); +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +// For documentation please check: +// http://requirejs.org/docs/api.html +requirejs.config({ + // Because require.js is included as a simple From 5990fa2ef5e382465f2d042efbe5a44eba5a4762 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Fri, 7 Dec 2012 17:25:24 +0200 Subject: [PATCH 241/541] Integrated RequireJS with xmodule for GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 ++++++- .../js/src/graphical_slider_tool/gst.js | 37 ++++++++++--------- .../js/src/graphical_slider_tool/gst_main.js | 17 +++++++++ .../{module.js => mod1.js} | 3 +- .../js/src/graphical_slider_tool/mod2.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod3.js | 19 ++++++++++ .../js/src/graphical_slider_tool/mod4.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod5.js | 16 ++++++++ 8 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{module.js => mod1.js} (88%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 60c03dec10..f89cb0f990 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -22,7 +22,19 @@ log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' - js = {'js': [resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')]} + + js = { + 'js': [ + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] + } js_module_name = "GraphicalSliderTool" def __init__(self, system, location, definition, descriptor, instance_state=None, diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js index 03778ea437..1434d05f70 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -1,17 +1,20 @@ -// Graphical Slider Tool module - -(function() { - this.GraphicalSliderTool = (function() { - function GST(el) { - console.log(el); - // element is : - //
    - } - // console.log('in GST'); - return GST; - - })(); -}).call(this); -// this=window, after call -// window['Graphical_Slider_Tool'] is available. \ No newline at end of file +/* + * We will add a function that will be called for all GraphicalSliderTool + * xmodule module instances. It must be available globally by design of + * xmodule. + */ +window.GraphicalSliderTool = function (el) { + // All the work will be performed by the GstMain module. We will get access + // to it, and all it's dependencies, via Require JS. Currently Require JS + // is namespaced and is available via a global object RequireJS. + RequireJS.require(['GstMain'], function (GstMain) { + // The GstMain module expects the DOM ID of a Graphical Slider Tool + // element. Since we are given a
    element which might in + // theory contain multiple graphical_slider_tool
    elements (each + // with a unique DOM ID), we will iterate over all children, and for + // each match, we will call GstMain module. + $(el).children('.graphical_slider_tool').each(function (index, value) { + GstMain($(value).attr('id')); + }); + }); +}; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js new file mode 100644 index 0000000000..66f98eddf7 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -0,0 +1,17 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { + return GstMain; + + function GstMain(gstId) { + console.log('The DOM ID of the current GST element is ' + gstId); + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js similarity index 88% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js index c4661b5e44..44674b96d3 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js @@ -2,7 +2,8 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define([], function () { +define('mod1', [], function () { + console.log('we are in the mod1 callback'); return { 'module_status': 'OK' }; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js new file mode 100644 index 0000000000..9c26bb1dfe --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod2', [], function () { + console.log('we are in the mod2 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js new file mode 100644 index 0000000000..21961f3611 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js @@ -0,0 +1,19 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod3', ['mod5'], function (mod5) { + console.log('we are in the mod3 callback'); + + console.log('mod5 status: [' + mod5.module_status + '].'); + + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js new file mode 100644 index 0000000000..0edf809155 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod4', [], function () { + console.log('we are in the mod4 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js new file mode 100644 index 0000000000..5e843ac468 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod5', [], function () { + console.log('we are in the mod5 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) From 080e96fdc481b95f9a50348c368872baacc17a12 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 08:28:34 +0200 Subject: [PATCH 242/541] Work in progress on GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 +- .../{mod3.js => general_methods.js} | 12 +- .../js/src/graphical_slider_tool/gst_main.js | 12 +- .../js/src/graphical_slider_tool/logme.js | 54 ++++++ .../js/src/graphical_slider_tool/mod1.js | 16 -- .../js/src/graphical_slider_tool/mod2.js | 16 -- .../js/src/graphical_slider_tool/mod4.js | 16 -- .../js/src/graphical_slider_tool/sliders.js | 142 +++++++++++++++ .../js/src/graphical_slider_tool/state.js | 165 ++++++++++++++++++ lms/templates/graphical_slider_tool.html | 16 +- 10 files changed, 395 insertions(+), 68 deletions(-) rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{mod3.js => general_methods.js} (65%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index f89cb0f990..3d7b8a9f02 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -26,10 +26,10 @@ class GraphicalSliderToolModule(XModule): js = { 'js': [ resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -128,7 +128,7 @@ class GraphicalSliderToolModule(XModule): Simple variant: slider and plot controls are not inside any tag. """ #substitute plot - plot_div = '
    \ This is plot
    ' html_string = html_string.replace('$plot$', plot_div) @@ -139,7 +139,7 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
    This is input
    ' for var in vars: diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js similarity index 65% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js index 21961f3611..9cdd4fff0f 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js @@ -2,12 +2,16 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('mod3', ['mod5'], function (mod5) { - console.log('we are in the mod3 callback'); - - console.log('mod5 status: [' + mod5.module_status + '].'); +define('GeneralMethods', [], function () { + if (!String.prototype.trim) { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + String.prototype.trim = function trim(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }; + } return { + 'module_name': 'GeneralMethods', 'module_status': 'OK' }; }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 66f98eddf7..9f2c4c356d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,11 +2,19 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { +define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { + logme(GeneralMethods); + return GstMain; function GstMain(gstId) { - console.log('The DOM ID of the current GST element is ' + gstId); + var config, state; + + config = JSON.parse($('#' + gstId + '_json').html()).root; + + state = State(gstId, config); + + Sliders(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js new file mode 100644 index 0000000000..c045757044 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js @@ -0,0 +1,54 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('logme', [], function () { + var debugMode; + + // debugMode can be one of the following: + // + // true - All messages passed to logme will be written to the internal + // browser console. + // false - Suppress all output to the internal browser console. + // + // Obviously, if anywhere there is a direct console.log() call, we can't do + // anything about it. That's why use logme() - it will allow to turn off + // the output of debug information with a single change to a variable. + debugMode = true; + + return logme; + + /* + * function: logme + * + * A helper function that provides logging facilities. We don't want + * to call console.log() directly, because sometimes it is not supported + * by the browser. Also when everything is routed through this function. + * the logging output can be easily turned off. + * + * logme() supports multiple parameters. Each parameter will be passed to + * console.log() function separately. + * + */ + function logme() { + var i; + + if ( + (typeof debugMode === 'undefined') || + (debugMode !== true) || + (typeof window.console === 'undefined') + ) { + return; + } + + for (i = 0; i < arguments.length; i++) { + window.console.log(arguments[i]); + } + } // End-of: function logme +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js deleted file mode 100644 index 44674b96d3..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod1', [], function () { - console.log('we are in the mod1 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js deleted file mode 100644 index 9c26bb1dfe..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod2', [], function () { - console.log('we are in the mod2 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js deleted file mode 100644 index 0edf809155..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod4', [], function () { - console.log('we are in the mod4 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js new file mode 100644 index 0000000000..6ef53bdbeb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -0,0 +1,142 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Sliders', ['logme'], function (logme) { + return Sliders; + + function Sliders(gstId, config, state) { + logme('We are inside Sliders function.'); + + logme('gstId: ' + gstId); + logme(config); + logme(state); + + // We will go through all of the sliders. For each one, we will make a + // jQuery UI slider for it, attach "on change" events, and set it's + // state - initial value, max, and min parameters. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + createSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + createSlider(config.sliders.slider); + } + } + + function createSlider(obj) { + var constName, constValue, rangeBlobs, valueMin, valueMax, + sliderDiv, sliderWidth; + + // The name of the constant is obj['@var']. Multiple sliders and/or + // inputs can represent the same constant - therefore we will get + // the most recent const value from the state object. The range is + // a string composed of 3 blobs, separated by commas. The first + // blob is the min value for the slider, the third blob is the max + // value for the slider. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + if (typeof obj['@range'] !== 'string') { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + // Get the first blob from the split string. + valueMin = parseFloat(rangeBlobs[0]); + + if (isNaN(valueMin) === true) { + valueMin = constValue - 10; + } + + // Get the third blob from the split string. + valueMax = parseFloat(rangeBlobs[2]); + + if (isNaN(valueMax) === true) { + valueMax = constValue + 10; + } + + // Logically, the min, value, and max should make sense. + // I.e. we will make sure that: + // + // min <= value <= max + // + // If this is not the case, we will set some defaults. + if ((valueMin > valueMax) || + (valueMin > constValue) || + (valueMax < constValue)) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } + } + } + + sliderDiv = $('#' + gstId + '_slider_' + constName); + + // If a corresponding slider DIV for this constant does not exist, + // do not do anything. + if (sliderDiv.length === 0) { + return; + } + + // The default slider width. + sliderWidth = 400; + + logme('width: 0'); + logme(obj['@width']); + if (typeof obj['@width'] === 'string') { + logme('width: 1'); + if (isNaN(parseInt(obj['@width'], 10)) === false) { + logme('width: 2'); + sliderWidth = parseInt(obj['@width'], 10); + } + } + + // Set the new width to the slider. + sliderDiv.width(sliderWidth); + + // Create a jQuery UI slider from the current DIV. We will set + // starting parameters, and will also attach a handler to update + // the state on the change event. + sliderDiv.slider({ + 'min': valueMin, + 'max': valueMax, + 'value': constValue, + + 'change': sliderOnChange + }); + + return; + + function sliderOnChange(event, ui) { + state.setConstValue(constName, ui.value); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js new file mode 100644 index 0000000000..17c8721a73 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -0,0 +1,165 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('State', ['logme'], function (logme) { + // Since there will be (can be) multiple GST on a page, and each will have + // a separate state, we will create a factory constructor function. The + // constructor will expect the ID of the DIV with the GST contents, and the + // configuration object (parsed from a JSON string). It will return and + // object containing methods to set and get the private state properties. + + // This module defines and returns a factory constructor. + return State; + + /* + * function: State + * + * + */ + function State(gstId, config) { + var constants, c1; + + constants = {}; + + // We must go through all of the input, and slider elements and + // retrieve all of the available constants. These will be added to an + // object as it's properties. + // + // First we will go through all of the inputs. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + addConstFromInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + addConstFromInput(config.inputs.input); + } + } + + // Now we will go through all of the sliders. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + addConstFromSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + addConstFromSlider(config.sliders.slider); + } + } + + logme(constants); + + // The constructor will return an object with methods to operate on + // it's private properties. + return { + 'getConstValue': getConstValue, + 'setConstValue': setConstValue + }; + + function getConstValue(constName) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + return constants[constName]; + } + + function setConstValue(constName, constValue) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + if (isNaN(parseFloat(constValue)) === true) { + // We are interested only in valid float values. + return; + } + + constants[constName] = parseFloat(constValue); + + logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + } + + function addConstFromInput(obj) { + var constName, constValue; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is obj['@initial']. I have taken the word 'initial' + // into brackets, because multiple inputs and/or sliders can + // represent the state of a single constant. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@initial'] === 'undefined') { + constValue = 0; + } else { + constValue = parseFloat(obj['@initial']); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + + constants[constName] = constValue; + } + + function addConstFromSlider(obj) { + var constName, constValue, rangeBlobs; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is the second blob of the 'range' parameter of the + // slider which is obj['@range']. Multiple sliders and/or inputs + // can represent the same constant - therefore 'initial' is in + // brackets. The range is a string composed of 3 blobs, separated + // by commas. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@range'] !== 'string') { + constValue = 0; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + constValue = 0; + } else { + // Get the second blob from the split string. + constValue = parseFloat(rangeBlobs[1]); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + } + + constants[constName] = constValue; + } + } // End-of: function State +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index d6cffc67e2..17d2bae5e9 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,12 +1,14 @@
    - -
    + + - -
    + + - + ${gst_html}
    From ce7a01dd26b0f407487b4c78a27bad559b9699ae Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:08:36 +0200 Subject: [PATCH 243/541] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 2 +- .../js/src/graphical_slider_tool/graph.js | 62 +++++++++++++++++++ .../js/src/graphical_slider_tool/gst_main.js | 7 ++- .../js/src/graphical_slider_tool/mod5.js | 16 ----- .../js/src/graphical_slider_tool/state.js | 8 +++ 5 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 3d7b8a9f02..633f5e9406 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,7 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') ] diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js new file mode 100644 index 0000000000..2aa19cfc02 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -0,0 +1,62 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Graph', ['logme'], function (logme) { + + return Graph; + + function Graph(gstId, state) { + var plotDiv, data; + logme('We are inside Graph module.', gstId, state); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 0) { + return; + } + + plotDiv.width(300); + plotDiv.height(300); + + plotDiv.bind('update_plot', function (event, forGstId) { + if (forGstId !== gstId) { + logme('update_plot event not for current ID'); + } + + logme('redrawing plot'); + + generateData(); + updatePlot(); + }); + + generateData(); + updatePlot(); + + return; + + function generateData() { + var a, b, c1; + + a = state.getConstValue('a'); + b = state.getConstValue('b'); + + data = []; + data.push([]); + + for (c1 = 0; c1 < 30; c1++) { + data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + } + } + + function updatePlot() { + $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 9f2c4c356d..68ef73e441 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,7 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { +define( + 'GstMain', + ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, logme, GeneralMethods, Sliders, Graph) { logme(GeneralMethods); return GstMain; @@ -15,6 +18,8 @@ define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (Sta state = State(gstId, config); Sliders(gstId, config, state); + + Graph(gstId, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js deleted file mode 100644 index 5e843ac468..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod5', [], function () { - console.log('we are in the mod5 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 17c8721a73..ffd618c51b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -76,6 +76,8 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { + var plotDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -90,6 +92,12 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 1) { + plotDiv.trigger('update_plot', [gstId]); + } } function addConstFromInput(obj) { From 7de575a84bf5f1915b5a58c0e0646e8c7f6e99e7 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:26:29 +0200 Subject: [PATCH 244/541] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 18 ++++++++---------- .../js/src/graphical_slider_tool/state.js | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 2aa19cfc02..fbd1f96da1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -19,22 +19,20 @@ define('Graph', ['logme'], function (logme) { plotDiv.width(300); plotDiv.height(300); - plotDiv.bind('update_plot', function (event, forGstId) { - if (forGstId !== gstId) { - logme('update_plot event not for current ID'); - } - - logme('redrawing plot'); - - generateData(); - updatePlot(); - }); + state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); generateData(); updatePlot(); return; + function onUpdatePlot(event) { + logme('redrawing plot'); + + generateData(); + updatePlot(); + } + function generateData() { var a, b, c1; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index ffd618c51b..735c100344 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -18,7 +18,7 @@ define('State', ['logme'], function (logme) { * */ function State(gstId, config) { - var constants, c1; + var constants, c1, plotDiv; constants = {}; @@ -62,9 +62,16 @@ define('State', ['logme'], function (logme) { // it's private properties. return { 'getConstValue': getConstValue, - 'setConstValue': setConstValue + 'setConstValue': setConstValue, + 'bindUpdatePlotEvent': bindUpdatePlotEvent }; + function bindUpdatePlotEvent(newPlotDiv, callback) { + plotDiv = newPlotDiv; + + plotDiv.bind('update_plot', callback); + } + function getConstValue(constName) { if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an @@ -76,8 +83,6 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { - var plotDiv; - if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -93,10 +98,8 @@ define('State', ['logme'], function (logme) { logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - plotDiv = $('#' + gstId + '_plot'); - - if (plotDiv.length === 1) { - plotDiv.trigger('update_plot', [gstId]); + if (plotDiv !== undefined) { + plotDiv.trigger('update_plot'); } } From 32c70a524c3a3dca1cb1939b6417f9677ee8809b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 11:37:55 +0200 Subject: [PATCH 245/541] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 95 ++++++++++++++++--- .../js/src/graphical_slider_tool/gst_main.js | 7 +- .../js/src/graphical_slider_tool/sliders.js | 12 +-- .../js/src/graphical_slider_tool/state.js | 34 +++++-- 4 files changed, 113 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index fbd1f96da1..762789cbf5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,12 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', ['logme'], function (logme) { +define('Graph', [], function () { return Graph; - function Graph(gstId, state) { - var plotDiv, data; - logme('We are inside Graph module.', gstId, state); + function Graph(gstId, config, state) { + var plotDiv, dataSets, functions; plotDiv = $('#' + gstId + '_plot'); @@ -21,34 +20,102 @@ define('Graph', ['logme'], function (logme) { state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); + createFunctions(); + generateData(); updatePlot(); return; - function onUpdatePlot(event) { - logme('redrawing plot'); + function createFunctions() { + functions = []; + if (typeof config.plot['function'] === 'undefined') { + return; + } + + if (typeof config.plot['function'] === 'string') { + addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { + + } else if ($.isArray(config.plot['function'])) { + + } + + return; + + function addFunction(funcString, color, line, dot, label, style, point_size) { + var newFunctionObject, func, constNames; + + if (typeof funcString !== 'string') { + return; + } + + newFunctionObject = {}; + + constNames = state.getAllConstantNames(); + + // The 'x' is always one of the function parameters. + constNames.push('x'); + + // Must make sure that the function body also gets passed to + // the Function cosntructor. + constNames.push(funcString); + + func = Function.apply(null, constNames); + newFunctionObject['func'] = func; + + if (typeof color === 'string') { + newFunctionObject['color'] = color; + } + + if (typeof line === 'boolean') { + newFunctionObject['line'] = line; + } + + if (typeof dot === 'boolean') { + newFunctionObject['dot'] = dot; + } + + if (typeof label === 'string') { + newFunctionObject['label'] = label; + } + + functions.push(newFunctionObject); + } + } + + function onUpdatePlot(event) { generateData(); updatePlot(); } function generateData() { - var a, b, c1; + var c0, c1, datapoints, constValues, x, y; - a = state.getConstValue('a'); - b = state.getConstValue('b'); + constValues = state.getAllConstantValues(); - data = []; - data.push([]); + dataSets = []; - for (c1 = 0; c1 < 30; c1++) { - data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + for (c0 = 0; c0 < functions.length; c0 += 1) { + datapoints = []; + + for (c1 = 0; c1 < 30; c1 += 0.1) { + x = c1; + // Push the 'x' variable to the end of the parameter array. + constValues.push(x); + y = functions[c0].func.apply(window, constValues); + constValues.pop(); + + datapoints.push([x, y]); + } + + dataSets.push(datapoints); } } function updatePlot() { - $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + $.plot(plotDiv, dataSets); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 68ef73e441..71de12b423 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,9 +4,8 @@ define( 'GstMain', - ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, logme, GeneralMethods, Sliders, Graph) { - logme(GeneralMethods); + ['State', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, GeneralMethods, Sliders, Graph) { return GstMain; @@ -19,7 +18,7 @@ define( Sliders(gstId, config, state); - Graph(gstId, state); + Graph(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 6ef53bdbeb..e871e9f035 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -2,16 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Sliders', ['logme'], function (logme) { +define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { - logme('We are inside Sliders function.'); - - logme('gstId: ' + gstId); - logme(config); - logme(state); - // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. @@ -102,12 +96,8 @@ define('Sliders', ['logme'], function (logme) { // The default slider width. sliderWidth = 400; - logme('width: 0'); - logme(obj['@width']); if (typeof obj['@width'] === 'string') { - logme('width: 1'); if (isNaN(parseInt(obj['@width'], 10)) === false) { - logme('width: 2'); sliderWidth = parseInt(obj['@width'], 10); } } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 735c100344..d632429c9b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -2,7 +2,7 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('State', ['logme'], function (logme) { +define('State', [], function () { // Since there will be (can be) multiple GST on a page, and each will have // a separate state, we will create a factory constructor function. The // constructor will expect the ID of the DIV with the GST contents, and the @@ -56,16 +56,40 @@ define('State', ['logme'], function (logme) { } } - logme(constants); - // The constructor will return an object with methods to operate on // it's private properties. return { 'getConstValue': getConstValue, 'setConstValue': setConstValue, - 'bindUpdatePlotEvent': bindUpdatePlotEvent + 'bindUpdatePlotEvent': bindUpdatePlotEvent, + 'getAllConstantNames': getAllConstantNames, + 'getAllConstantValues': getAllConstantValues }; + function getAllConstantNames() { + var constName, allConstNames; + + allConstNames = []; + + for (constName in constants) { + allConstNames.push(constName); + } + + return allConstNames; + } + + function getAllConstantValues() { + var constName, allConstValues; + + allConstValues = []; + + for (constName in constants) { + allConstValues.push(constants[constName]); + } + + return allConstValues; + } + function bindUpdatePlotEvent(newPlotDiv, callback) { plotDiv = newPlotDiv; @@ -96,8 +120,6 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); - logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } From ae03090f3c980ce5b7c7ee5862f31acc86b08773 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 12:43:01 +0200 Subject: [PATCH 246/541] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 ++--- .../xmodule/xmodule/js/src/graphical_slider_tool/graph.js | 8 +++++++- .../xmodule/js/src/graphical_slider_tool/sliders.js | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 633f5e9406..c07c0670d7 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -129,8 +129,7 @@ class GraphicalSliderToolModule(XModule): """ #substitute plot plot_div = '
    \ - This is plot
    ' + style="width: 600px; height: 600px; padding: 0px; position: relative;">This is plot
    ' html_string = html_string.replace('$plot$', plot_div) # substitute sliders @@ -140,7 +139,7 @@ class GraphicalSliderToolModule(XModule): vars = [x['@var'] for x in sliders] slider_div = '
    This is slider
    ' + data-var="{var}">
    ' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 762789cbf5..991cb0a26e 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -28,6 +28,8 @@ define('Graph', [], function () { return; function createFunctions() { + var c1; + functions = []; if (typeof config.plot['function'] === 'undefined') { @@ -39,7 +41,11 @@ define('Graph', [], function () { } else if ($.isPlainObject(config.plot['function']) === true) { } else if ($.isArray(config.plot['function'])) { - + for (c1 = 0; c1 < config.plot['function'].length; c1++) { + if (typeof config.plot['function'][c1] === 'string') { + addFunction(config.plot['function'][c1]); + } + } } return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index e871e9f035..226f53d696 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -112,6 +112,7 @@ define('Sliders', [], function () { 'min': valueMin, 'max': valueMax, 'value': constValue, + 'step': 0.01, 'change': sliderOnChange }); From 3e9d325a9f166901a2e24bc2dfe3942173f892c8 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 14:07:06 +0200 Subject: [PATCH 247/541] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 +- .../js/src/graphical_slider_tool/gst_main.js | 5 +- .../js/src/graphical_slider_tool/inputs.js | 70 +++++++++++++++++++ .../js/src/graphical_slider_tool/sliders.js | 1 + .../js/src/graphical_slider_tool/state.js | 7 ++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index c07c0670d7..9e9273bc25 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,6 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -154,8 +155,8 @@ class GraphicalSliderToolModule(XModule): inputs = [inputs] vars = [x['@var'] for x in inputs] - input_div = '
    This is input
    ' + input_div = '' for var in vars: html_string = re.sub(r'\$input\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 71de12b423..47881b66c6 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,8 +4,8 @@ define( 'GstMain', - ['State', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, GeneralMethods, Sliders, Graph) { + ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], + function (State, GeneralMethods, Sliders, Inputs, Graph) { return GstMain; @@ -17,6 +17,7 @@ define( state = State(gstId, config); Sliders(gstId, config, state); + Inputs(gstId, config, state); Graph(gstId, config, state); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js new file mode 100644 index 0000000000..5b9f1f87c2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -0,0 +1,70 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Inputs', ['logme'], function (logme) { + return Inputs; + + function Inputs(gstId, config, state) { + logme('Inside "Inputs" module.'); + logme(gstId, config, state); + + // We will go thorugh all of the inputs, and those that have a valid + // '@var' property will be added to the page as a HTML text input + // element. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + createInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + createInput(config.inputs.input); + } + } + + function createInput(obj) { + var constName, constValue, inputDiv, textInputDiv; + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + inputDiv = $('#' + gstId + '_input_' + constName); + + if (inputDiv.length === 0) { + return; + } + + textInputDiv = $(''); + textInputDiv.width(50); + + textInputDiv.appendTo(inputDiv); + textInputDiv.val(constValue); + + textInputDiv.bind('change', inputOnChange); + + return; + + function inputOnChange(event) { + state.setConstValue(constName, $(this).val()); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 226f53d696..51bd2c8b12 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -104,6 +104,7 @@ define('Sliders', [], function () { // Set the new width to the slider. sliderDiv.width(sliderWidth); + sliderDiv.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index d632429c9b..88951f0e9d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -107,6 +107,8 @@ define('State', [], function () { } function setConstValue(constName, constValue) { + var inputDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -123,6 +125,11 @@ define('State', [], function () { if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } + + inputDiv = $('#' + gstId + '_input_' + constName).children('input'); + if (inputDiv.length !== 0) { + inputDiv.val(constValue); + } } function addConstFromInput(obj) { From b08b25b98388e4f42f501c930f8d7a4490fca960 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 16:11:29 +0200 Subject: [PATCH 248/541] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 4 +-- .../js/src/graphical_slider_tool/inputs.js | 27 ++++++++++++------- .../js/src/graphical_slider_tool/sliders.js | 20 ++++++++------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 9e9273bc25..61a883fbf8 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -139,8 +139,8 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
    ' + slider_div = '' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index 5b9f1f87c2..d7f64328e0 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -27,9 +27,9 @@ define('Inputs', ['logme'], function (logme) { } function createInput(obj) { - var constName, constValue, inputDiv, textInputDiv; + var constName, constValue, spanEl, inputEl; - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -40,19 +40,28 @@ define('Inputs', ['logme'], function (logme) { constValue = 0; } - inputDiv = $('#' + gstId + '_input_' + constName); + spanEl = $('#' + gstId + '_input_' + constName); - if (inputDiv.length === 0) { + if (spanEl.length === 0) { return; } - textInputDiv = $(''); - textInputDiv.width(50); + inputEl = $(''); - textInputDiv.appendTo(inputDiv); - textInputDiv.val(constValue); + // inputEl.width(50); + inputEl.val(constValue); + inputEl.bind('change', inputOnChange); + inputEl.button().css({ + 'font': 'inherit', + 'color': 'inherit', + 'text-align': 'left', + 'outline': 'none', + 'cursor': 'text', + 'height': '15px', + 'width': '50px' + }); - textInputDiv.bind('change', inputOnChange); + inputEl.appendTo(spanEl); return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 51bd2c8b12..3db0c3e67c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -25,7 +25,7 @@ define('Sliders', [], function () { function createSlider(obj) { var constName, constValue, rangeBlobs, valueMin, valueMax, - sliderDiv, sliderWidth; + spanEl, sliderEl, sliderWidth; // The name of the constant is obj['@var']. Multiple sliders and/or // inputs can represent the same constant - therefore we will get @@ -34,7 +34,7 @@ define('Sliders', [], function () { // blob is the min value for the slider, the third blob is the max // value for the slider. - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -85,14 +85,16 @@ define('Sliders', [], function () { } } - sliderDiv = $('#' + gstId + '_slider_' + constName); + spanEl = $('#' + gstId + '_slider_' + constName); // If a corresponding slider DIV for this constant does not exist, // do not do anything. - if (sliderDiv.length === 0) { + if (spanEl.length === 0) { return; } + sliderEl = $('
    '); + // The default slider width. sliderWidth = 400; @@ -103,21 +105,23 @@ define('Sliders', [], function () { } // Set the new width to the slider. - sliderDiv.width(sliderWidth); - sliderDiv.css('display', 'inline-block'); + sliderEl.width(sliderWidth); + sliderEl.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update // the state on the change event. - sliderDiv.slider({ + sliderEl.slider({ 'min': valueMin, 'max': valueMax, 'value': constValue, - 'step': 0.01, + 'step': (valueMax - valueMin) / 50.0, 'change': sliderOnChange }); + sliderEl.appendTo(spanEl); + return; function sliderOnChange(event, ui) { From 3682cb46c4522bee93c6acafa7e38c02cc052403 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 18:37:20 +0200 Subject: [PATCH 249/541] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 15 +- .../js/src/graphical_slider_tool/inputs.js | 72 +++++++-- .../js/src/graphical_slider_tool/sliders.js | 137 ++++++++++++++---- 3 files changed, 186 insertions(+), 38 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 991cb0a26e..61228413f5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -121,7 +121,20 @@ define('Graph', [], function () { } function updatePlot() { - $.plot(plotDiv, dataSets); + $.plot( + plotDiv, + dataSets, + { + 'xaxis': { + 'min': 0, + 'max': 30 + }, + 'yaxis': { + 'min': -5, + 'max': 5 + } + } + ); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index d7f64328e0..3e7e55f02c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -2,12 +2,24 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Inputs', ['logme'], function (logme) { +define('Inputs', [], function () { return Inputs; function Inputs(gstId, config, state) { - logme('Inside "Inputs" module.'); - logme(gstId, config, state); + var constNamesUsed; + + // There should not be more than one text input per a constant. This + // just does not make sense. However, nothing really prevents the user + // from specifying more than one text input for the same constant name. + // That's why we have to track which constant names already have + // text inputs for them, and prevent adding further text inputs to + // these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a text input to. + // When creating a new text input, we must consult with this object, to + // see if the constant name is not defined as it's property. + constNamesUsed = {}; // We will go thorugh all of the inputs, and those that have a valid // '@var' property will be added to the page as a HTML text input @@ -15,42 +27,75 @@ define('Inputs', ['logme'], function (logme) { if ((typeof config.inputs !== 'undefined') && (typeof config.inputs.input !== 'undefined')) { if ($.isArray(config.inputs.input)) { - // config.inputs.input is an array + // config.inputs.input is an array. For each element, we will + // add a text input. for (c1 = 0; c1 < config.inputs.input.length; c1++) { createInput(config.inputs.input[c1]); } } else if ($.isPlainObject(config.inputs.input)) { - // config.inputs.input is an object + + // config.inputs.input is an object. Add a text input for it. createInput(config.inputs.input); + } } function createInput(obj) { var constName, constValue, spanEl, inputEl; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a text input for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a text input for a constant which already has a + // text input defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, we will skip creating a text input + // for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // With the constant name, and the constant value being defined, + // lets get the element on the page into which the text input will + // be inserted. spanEl = $('#' + gstId + '_input_' + constName); + // If a corresponding element for this constant does not exist on + // the page, we will not be making a text input. if (spanEl.length === 0) { return; } + // Create the text input element. inputEl = $(''); - // inputEl.width(50); + // Set the current constant to the text input. It will be visible + // to the user. inputEl.val(constValue); + + // Bind a function to the 'change' event. Whenever the user changes + // the value of this text input, and presses 'enter' (or clicks + // somewhere else on the page), this event will be triggered, and + // our callback will be called. inputEl.bind('change', inputOnChange); + + // Lets style the input element nicely. We will use the button() + // widget for this since there is no native widget for the text + // input. inputEl.button().css({ 'font': 'inherit', 'color': 'inherit', @@ -61,10 +106,17 @@ define('Inputs', ['logme'], function (logme) { 'width': '50px' }); + // And finally, publish the text input element to the page. inputEl.appendTo(spanEl); + // Don't forget to add the constant to the list of used constants. + // Next time a slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // When the user changes the value of this text input, the 'state' + // will be updated, forcing the plot to be redrawn. function inputOnChange(event) { state.setConstValue(constName, $(this).val()); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 3db0c3e67c..33bdd89dd1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -6,68 +6,117 @@ define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { + var constNamesUsed; + + // There should not be more than one slider per a constant. This just + // does not make sense. However, nothing really prevents the user from + // specifying more than one slider for the same constant name. That's + // why we have to track which constant names already have sliders for + // them, and prevent adding further sliders to these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a slider to. When + // creating a new slider, we must consult with this object, to see if + // the constant name is not defined as it's property. + constNamesUsed = {}; + // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. if ((typeof config.sliders !== 'undefined') && (typeof config.sliders.slider !== 'undefined')) { if ($.isArray(config.sliders.slider)) { - // config.sliders.slider is an array + // config.sliders.slider is an array. For each object in the + // array, create a slider. for (c1 = 0; c1 < config.sliders.slider.length; c1++) { createSlider(config.sliders.slider[c1]); } + } else if ($.isPlainObject(config.sliders.slider)) { - // config.sliders.slider is an object + + // config.sliders.slider is an object. Create a slider for it. createSlider(config.sliders.slider); + } } function createSlider(obj) { - var constName, constValue, rangeBlobs, valueMin, valueMax, - spanEl, sliderEl, sliderWidth; - - // The name of the constant is obj['@var']. Multiple sliders and/or - // inputs can represent the same constant - therefore we will get - // the most recent const value from the state object. The range is - // a string composed of 3 blobs, separated by commas. The first - // blob is the min value for the slider, the third blob is the max - // value for the slider. + var constName, constValue, rangeBlobs, valueMin, valueMax, spanEl, + sliderEl, sliderWidth; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a slider for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a slider for a constant which already has a + // slider defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, then something terrible has + // happened! We will skip creating a slider for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // The range is a string composed of 3 blobs, separated by commas. + // The first blob is the min value for the slider, the third blob + // is the max value for the slider. if (typeof obj['@range'] !== 'string') { + + // If the range is not a string, we will set a default range. + // No promise as to the quality of the data points that this + // range will produce. valueMin = constValue - 10; valueMax = constValue + 10; + } else { + + // Separate the range string by commas, and store each blob as + // an element in an array. rangeBlobs = obj['@range'].split(','); // We must have gotten exactly 3 blobs (pieces) from the split. if (rangeBlobs.length !== 3) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if the range string was + // split into more or less than 3 pieces. + setDefaultMinMax(); + } else { - // Get the first blob from the split string. + + // Get the first blob from the split string. It is the min + // value. valueMin = parseFloat(rangeBlobs[0]); + // Is it a well-formed float number? if (isNaN(valueMin) === true) { + + // No? Then set a sensible default value. valueMin = constValue - 10; + } - // Get the third blob from the split string. + // Get the third blob from the split string. It is the max. valueMax = parseFloat(rangeBlobs[2]); + // Is it a well-formed float number? if (isNaN(valueMax) === true) { + + // No? Then set a sensible default value. valueMax = constValue + 10; + } // Logically, the min, value, and max should make sense. @@ -79,38 +128,53 @@ define('Sliders', [], function () { if ((valueMin > valueMax) || (valueMin > constValue) || (valueMax < constValue)) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if min/value/max logic + // is broken. + setDefaultMinMax(); + } } } + // At this point we have the constant name, the constant value, and + // the min and max values for this slider. Lets get the element on + // the page into which the slider will be inserted. spanEl = $('#' + gstId + '_slider_' + constName); - // If a corresponding slider DIV for this constant does not exist, - // do not do anything. + // If a corresponding element for this constant does not exist on + // the page, we will not be making a slider. if (spanEl.length === 0) { return; } + // Create the slider DIV. sliderEl = $('
    '); - // The default slider width. + // We will define the width of the slider to a sensible default. sliderWidth = 400; + // Then we will see if one is provided in the config for this + // slider. If we find it, and it is a well-formed integer, we will + // use it, instead of the default width. if (typeof obj['@width'] === 'string') { if (isNaN(parseInt(obj['@width'], 10)) === false) { sliderWidth = parseInt(obj['@width'], 10); } } - // Set the new width to the slider. + // Set the defined width to the slider. sliderEl.width(sliderWidth); + + // And make sure that it gets added to the page as an + // 'inline-block' element. This will allow for the insertion of the + // slider into a paragraph, without the browser forcing it out of + // the paragraph onto a new line, separate line. sliderEl.css('display', 'inline-block'); - // Create a jQuery UI slider from the current DIV. We will set + // Create a jQuery UI slider from the slider DIV. We will set // starting parameters, and will also attach a handler to update - // the state on the change event. + // the 'state' on the 'change' event. sliderEl.slider({ 'min': valueMin, 'max': valueMax, @@ -120,13 +184,32 @@ define('Sliders', [], function () { 'change': sliderOnChange }); + // Append the slider DIV to the element on the page where the user + // wants to see it. sliderEl.appendTo(spanEl); + // OK! So we made it this far... + // + // Adding the constant to the list of used constants. Next time a + // slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // Update the 'state' - i.e. set the value of the constant this + // slider is attached to to a new value. + // + // This will cause the plot to be redrawn each time after the user + // drags the slider handle and releases it. function sliderOnChange(event, ui) { state.setConstValue(constName, ui.value); } + + // The sensible defaults for the slider's range. + function setDefaultMinMax() { + valueMin = constValue - 10; + valueMax = constValue + 10; + } } } }); From 28f4921924c1e47c7a68a754292a63dcdea6c35e Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 06:56:43 +0200 Subject: [PATCH 250/541] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 61228413f5..dba0483674 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,15 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', [], function () { +define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { var plotDiv, dataSets, functions; + logme(config); + plotDiv = $('#' + gstId + '_plot'); if (plotDiv.length === 0) { @@ -39,11 +41,29 @@ define('Graph', [], function () { if (typeof config.plot['function'] === 'string') { addFunction(config.plot['function']); } else if ($.isPlainObject(config.plot['function']) === true) { - + addFunction( + config.plot['function']['#text'], + config.plot['function']['@color'], + config.plot['function']['@dot'], + config.plot['function']['@label'], + config.plot['function']['@line'], + config.plot['function']['@point_size'], + config.plot['function']['@style'] + ); } else if ($.isArray(config.plot['function'])) { for (c1 = 0; c1 < config.plot['function'].length; c1++) { if (typeof config.plot['function'][c1] === 'string') { addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { + addFunction( + config.plot['function'][c1]['#text'], + config.plot['function'][c1]['@color'], + config.plot['function'][c1]['@dot'], + config.plot['function'][c1]['@label'], + config.plot['function'][c1]['@line'], + config.plot['function'][c1]['@point_size'], + config.plot['function'][c1]['@style'] + ); } } } @@ -76,17 +96,31 @@ define('Graph', [], function () { } if (typeof line === 'boolean') { - newFunctionObject['line'] = line; + if ((line === 'true') || (line === true)) { + newFunctionObject['line'] = true; + } else { + newFunctionObject['line'] = false; + } } - if (typeof dot === 'boolean') { - newFunctionObject['dot'] = dot; + if ((typeof dot === 'boolean') || (typeof dot === 'string')) { + if ((dot === 'true') || (dot === true)) { + newFunctionObject['dot'] = true; + } else { + newFunctionObject['dot'] = false; + } + } + + if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { + newFunctionObject['line'] = true; } if (typeof label === 'string') { newFunctionObject['label'] = label; } + logme(newFunctionObject); + functions.push(newFunctionObject); } } From 3e46ecef646f194798dff81dc4f4176a9e69a27f Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 10:47:46 +0200 Subject: [PATCH 251/541] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 132 ++++++++++++++---- .../js/src/graphical_slider_tool/gst_main.js | 4 + .../js/graphical_slider_tool/gst_module.js | 15 -- .../static/js/graphical_slider_tool/main.js | 75 ---------- 4 files changed, 108 insertions(+), 118 deletions(-) delete mode 100644 common/static/js/graphical_slider_tool/gst_module.js delete mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index dba0483674..c0c8addf80 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -7,7 +7,7 @@ define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { - var plotDiv, dataSets, functions; + var plotDiv, dataSeries, functions; logme(config); @@ -39,37 +39,55 @@ define('Graph', ['logme'], function (logme) { } if (typeof config.plot['function'] === 'string') { + + // If just one function string is present. addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { - addFunction( - config.plot['function']['#text'], - config.plot['function']['@color'], - config.plot['function']['@dot'], - config.plot['function']['@label'], - config.plot['function']['@line'], - config.plot['function']['@point_size'], - config.plot['function']['@style'] - ); + + // If a function is present, but it also has properties + // defined. + callAddFunction(config.plot['function']); + } else if ($.isArray(config.plot['function'])) { + + // If more than one function is defined. for (c1 = 0; c1 < config.plot['function'].length; c1++) { + + // For each definition, we must check if it is a simple + // string definition, or a complex one with properties. if (typeof config.plot['function'][c1] === 'string') { + + // Simple string. addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { - addFunction( - config.plot['function'][c1]['#text'], - config.plot['function'][c1]['@color'], - config.plot['function'][c1]['@dot'], - config.plot['function'][c1]['@label'], - config.plot['function'][c1]['@line'], - config.plot['function'][c1]['@point_size'], - config.plot['function'][c1]['@style'] - ); + + // Properties are present. + callAddFunction(config.plot['function'][c1]); + } } } return; + // This function will reduce code duplications. We have to call + // the function addFunction() several times passing object + // properties. A parameters. Rather than writing them out every + // time, we will have a single point of + function callAddFunction(obj) { + addFunction( + obj['#text'], + obj['@color'], + obj['@line'], + obj['@dot'], + obj['@label'], + obj['@style'], + obj['@point_size'] + ); + } + function addFunction(funcString, color, line, dot, label, style, point_size) { var newFunctionObject, func, constNames; @@ -95,7 +113,7 @@ define('Graph', ['logme'], function (logme) { newFunctionObject['color'] = color; } - if (typeof line === 'boolean') { + if ((typeof line === 'boolean') || (typeof line === 'string')) { if ((line === 'true') || (line === true)) { newFunctionObject['line'] = true; } else { @@ -111,6 +129,9 @@ define('Graph', ['logme'], function (logme) { } } + // By default, if no preference was set, or if the preference + // is conflicting (we must have either line or dot, none is + // not an option), we will show line. if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { newFunctionObject['line'] = true; } @@ -131,33 +152,68 @@ define('Graph', ['logme'], function (logme) { } function generateData() { - var c0, c1, datapoints, constValues, x, y; + var c0, c1, functionObj, seriesObj, dataPoints, constValues, x, y; constValues = state.getAllConstantValues(); - dataSets = []; + dataSeries = []; for (c0 = 0; c0 < functions.length; c0 += 1) { - datapoints = []; + functionObj = functions[c0]; + logme('Functions obj:', functionObj); - for (c1 = 0; c1 < 30; c1 += 0.1) { + seriesObj = {}; + dataPoints = []; + + for (c1 = 0; c1 < 30; c1 += 1) { x = c1; + // Push the 'x' variable to the end of the parameter array. constValues.push(x); - y = functions[c0].func.apply(window, constValues); + + // We call the user defined function, passing all of the + // available constant values. inside this function they + // will be accessible by their names. + y = functionObj.func.apply(window, constValues); + + // Return the constValues array to how it was before we + // added 'x' variable to the end of it. constValues.pop(); - datapoints.push([x, y]); + // Add the generated point to the data points set. + dataPoints.push([x, y]); + } - dataSets.push(datapoints); + // Put the entire data points set into the series object. + seriesObj.data = dataPoints; + + // See if user defined a specific color for this function. + if (functionObj.hasOwnProperty('color') === true) { + seriesObj.color = functionObj.color; + } + + // See if a user defined a label for this function. + if (functionObj.hasOwnProperty('label') === true) { + seriesObj.label = functionObj.label; + } + + seriesObj.lines = { + 'show': functionObj.line + }; + + seriesObj.points = { + 'show': functionObj.dot + }; + + dataSeries.push(seriesObj); } } function updatePlot() { $.plot( plotDiv, - dataSets, + dataSeries, { 'xaxis': { 'min': 0, @@ -166,9 +222,29 @@ define('Graph', ['logme'], function (logme) { 'yaxis': { 'min': -5, 'max': 5 + }, + 'legend': { + + // To show the legend or not. Note, even if 'show' is + // 'true', the legend will only show if labels are + // provided for at least one of the series that are + // going to be plotted. + 'show': true, + + // A floating point number in the range [0, 1]. The + // smaller the number, the more transparent will the + // legend background become. + 'backgroundOpacity': 0 + } } ); + + MathJax.Hub.Queue([ + 'Typeset', + MathJax.Hub, + plotDiv.attr('id') + ]); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 47881b66c6..8611fed1f2 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,6 +4,10 @@ define( 'GstMain', + + // Even though it is not explicitly in this module, we have to specify + // 'GeneralMethods' as a dependency. It expands some of the core JS objects + // with additional useful methods that are used in other modules. ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], function (State, GeneralMethods, Sliders, Inputs, Graph) { diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js deleted file mode 100644 index c4661b5e44..0000000000 --- a/common/static/js/graphical_slider_tool/gst_module.js +++ /dev/null @@ -1,15 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define([], function () { - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js deleted file mode 100644 index da36d9c9d6..0000000000 --- a/common/static/js/graphical_slider_tool/main.js +++ /dev/null @@ -1,75 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -// For documentation please check: -// http://requirejs.org/docs/api.html -requirejs.config({ - // Because require.js is included as a simple - -
    +
    +
    +

    Organization

    +
    +

    What is edX?

    +

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

    +

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

    +
    -
    -

    edX Basics

    -

    How do I sign up to take a class?

    -
    -

    Simply create an edX account (it's free) and then register for the course of your choice (also free). Follow the prompts on the edX website.

    -
    -
    -
    -

    What does it cost to take a class? Is this really free?

    -
    -

    EdX courses are free for everyone. All you need is an Internet connection.

    -
    -
    -
    -

    What happens after I sign up for a course?

    -
    -

    You will receive an activation email. Follow the prompts in that email to activate your account. You will need to log in each time you access your course(s). Once the course begins, it’s time to hit the virtual books. You can access the lectures, homework, tutorials, etc., for each week, one week at a time.

    -
    -
    -
    -

    Who can take an edX course?

    -
    -

    You, your mom, your little brother, your grandfather -- anyone with Internet access can take an edX course. Free.

    -
    -
    -
    -

    Are the courses only offered in English?

    -
    -

    Some edX courses include a translation of the lecture in the text bar to the right of the video. Some have the specific option of requesting a course in other languages. Please check your course to determine foreign language options.

    -
    -
    -
    -

    When will there be more courses on other subjects?

    -
    -

    We are continually reviewing and creating courses to add to the edX platform. Please check the website for future course announcements. You can also "friend" edX on Facebook – you’ll receive updates and announcements.

    -
    -
    -
    -

    How can I help edX?

    -
    -

    You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.

    -
    -
    -
    -

    When does my course start and/or finish?

    -
    -

    You can find the start and stop dates for each course on each course description page.

    -
    -
    -
    -

    Is there a walk-through of a sample course session?

    -
    -

    There are video introductions for every course that will give you a good sense of how the course works and what to expect.

    -
    -
    -
    -

    I don't have the prerequisites for a course that I am interested in. Can I still take the course?

    -
    -

    We do not check students for prerequisites, so you are allowed to attempt the course. However, if you do not know prerequisite subjects before taking a class, you will have to learn the prerequisite material on your own over the semester, which can be an extremely difficult task.

    -
    -
    -
    -

    What happens if I have to quit a course, are there any penalties, will I be able to take another course in the future?

    -
    -

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    -
    +

    Will edX be adding additional X Universities?

    +

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

    +

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

    -
    -

    The Classes

    +
    +

    Students

    -

    How much work will I have to do to pass my course?

    -
    -

    The projected hours of study required for each course are described on the specific course description page.

    -
    +

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

    +

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

    -

    What should I do before I take a course (prerequisites)?

    -
    -

    Each course is different – some have prerequisites, and some don’t. Take a look at your specific course’s recommended prerequisites. If you do not have a particular prerequisite, you may still take the course.

    -
    +

    Will certificates be awarded?

    +

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

    -

    What books should I read? (I am interested in reading materials before the class starts).

    -
    -

    Take a look at the specific course prerequisites. All required academic materials will be provided during the course, within the browser. Some of the course descriptions may list additional resources. For supplemental reading material before or during the course, you can post a question on the course’s Discussion Forum to ask your online coursemates for suggestions.

    -
    +

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

    +

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

    -

    Can I download the book for my course?

    -
    -

    EdX book content may only be viewed within the browser, and downloading it violates copyright laws. If you need or want a hard copy of the book, we recommend that you purchase a copy.

    -
    +

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

    +

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

    -

    Can I take more than one course at a time?

    -
    -

    You may take multiple edX courses, however we recommend checking the requirements on each course description page to determine your available study hours and the demands of the intended courses.

    -
    +

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

    +

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

    -

    How do I log in to take an edX class?

    -
    -

    Once you sign up for a course and activate your account, click on the "Log In" button on the edx.org home page. You will need to type in your email address and edX password each time you log in.

    -
    +

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

    +

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

    -

    What time is the class?

    -
    -

    EdX classes take place at your convenience. Prefer to sleep in and study late? No worries. Videos and problem sets are available 24 hours a day, which means you can watch video and complete work whenever you have spare time. You simply log in to your course via the Internet and work through the course material, one week at a time.

    -
    +

    How may I apply to study with edX?

    +

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

    -

    If I miss a week, how does this affect my grade?

    -
    -

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    -
    -
    -
    -

    How can I meet/find other students?

    -
    -

    All edX courses have Discussion Forums where you can chat with and help each other within the framework of the Honor Code.

    -
    -
    -
    -

    How can I talk to professors, fellows and teaching assistants?

    -
    -

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    -
    +

    How may another university participate in edX?

    +

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

    -
    -

    Getting Help

    +
    +

    Technology Platform

    -

    Can I re-take a course?

    -
    -

    Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.

    -
    +

    What technology will edX use?

    +

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

    +

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

    -

    Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?

    -
    -

    Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.

    -
    -
    -
    -

    Is there an exam at the end?

    -
    -

    Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.

    -
    -
    -
    -

    Will the same courses be offered again in the future?

    -
    -

    Existing edX courses will be re-offered, and more courses added.

    -
    -
    -
    -

    Will I get a certificate for taking an edX course?

    -
    -

    Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.

    -
    -
    -
    -

    How are edX certificates delivered?

    -
    -

    EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.

    -
    -
    -
    -

    What is the difference between a proctored certificate and an honor code certificate?

    -
    -

    A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed the edX honor code .

    -
    -
    -
    -

    Yes. The requirements for both certificates can be independently satisfied.

    -
    -

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    -
    -
    -
    -

    Will my grade be shown on my certificate?

    -
    -

    No. Grades are not displayed on either honor code or proctored certificates.

    -
    -
    -
    -

    How can I talk to professors, fellows and teaching assistants?

    -
    -

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    -
    -
    -
    -

    The only certificates distributed with grades by edX were for the initial prototype course.

    -
    -

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    -
    -
    -
    -

    Will my university accept my edX coursework for credit?

    -
    -

    Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.

    -
    -
    -
    -

    I lost my edX certificate – can you resend it to me?

    -
    -

    Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.

    -
    -
    -
    - -
    -

    edX & Open Source

    -
    -

    What’s open source?

    -
    -

    Open source is a philosophy that generally refers to making software freely available for use or modification as users see fit. In exchange for use of the software, users generally add their contributions to the software, making it a public collaboration. The edX platform will be made available as open source code in order to allow world talent to improve and share it on an ongoing basis.

    -
    -
    -
    -

    When/how can I get the open-source platform technology?

    -
    -

    We are still building the edX technology platform and will be making announcements in the future about its availability.

    -
    -
    -
    - -
    -

    Other Help Questions - Account Questions

    -
    -

    My username is taken.

    -
    -

    Now’s your chance to be creative: please try a different, more unique username – for example, try adding a random number to the end.

    -
    -
    -
    -

    Why does my password show on my course login page?

    -
    -

    Oops! This may be because of the way you created your account. For example, you may have mistakenly typed your password into the login box.

    -
    -
    -
    -

    I am having login problems (password/email unrecognized).

    -
    -

    Please check your browser’s settings to make sure that you have the current version of Firefox or Chrome, and then try logging in again. If you find access impossible, you may simply create a new account using an alternate email address – the old, unused account will disappear later.

    -
    -
    -
    -

    I did not receive an activation email.

    -
    -

    If you did not receive an activation email it may be because:

    -
      -
    • There was a typo in your email address.
    • -
    • Your spam filter may have caught the activation email. Please check your spam folder.
    • -
    • You may be using an older browser. We recommend downloading the current version of Firefox or Chrome.
    • -
    • JavaScript is disabled in your browser. Please check your browser settings and confirm that JavaScript is enabled.
    • -
    -

    If you continue to have problems activating your account, we recommend that you try creating a new account. There is no need to do anything about the old account. If it is not activated through the link in the email, it will disappear later.

    -
    -
    -
    -

    Can I delete my account?

    -
    -

    There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.

    -
    -
    -
    -

    I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.

    -
    -

    Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.

    -
    +

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

    +

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

    -
    diff --git a/lms/templates/static_templates/help.html b/lms/templates/static_templates/help.html index 7d1748776c..04c9164289 100644 --- a/lms/templates/static_templates/help.html +++ b/lms/templates/static_templates/help.html @@ -5,40 +5,304 @@ <%block name="title">edX Help +<%block name="js_extra"> + + +

    Help


    -
    -

    I tried to sign up, but it says the username is already taken.

    -

    If you have previously signed up for an MITx account, you already have an edX account and can log in with your existing username and password. If you don’t have an MITx account and received this error, it's possible that someone else has already signed up with that username. Please try a different, more unique username – for example, try adding a random number to the end.

    -
    -
    -

    How will I know that the course I have signed up for has started?

    -

    The start date for each course is listed on the right-hand side of the Course About page.

    -
    -
    -

    I just signed up into edX. I have not received any form of acknowledgement that I have enrolled.

    -

    You should receive a single activation e-mail. If you did not, it may be because:

    -
      -
    • There was a typo in your e-mail address.
    • -
    • The activation e-mail was caught by your spam filter. Please check your spam folder.
    • -
    • You may be using an older browser. We recommend downloading the current version of Firefox or Chrome.
    • -
    • JavaScript is disabled in your browser. Please confirm it is enabled.
    • -
    • If you run into issues, try recreating your account. There is no need to do anything about the old account, if any. If it is not activated through the link in the e-mail, it will disappear later.
    • -
    -
    -
    +
    +
    +

    edX Basics

    +
    +

    How do I sign up to take a class?

    +
    +

    Simply create an edX account (it's free) and then register for the course of your choice (also free). Follow the prompts on the edX website.

    +
    +
    +
    +

    What does it cost to take a class? Is this really free?

    +
    +

    EdX courses are free for everyone. All you need is an Internet connection.

    +
    +
    +
    +

    What happens after I sign up for a course?

    +
    +

    You will receive an activation email. Follow the prompts in that email to activate your account. You will need to log in each time you access your course(s). Once the course begins, it’s time to hit the virtual books. You can access the lectures, homework, tutorials, etc., for each week, one week at a time.

    +
    +
    +
    +

    Who can take an edX course?

    +
    +

    You, your mom, your little brother, your grandfather -- anyone with Internet access can take an edX course. Free.

    +
    +
    +
    +

    Are the courses only offered in English?

    +
    +

    Some edX courses include a translation of the lecture in the text bar to the right of the video. Some have the specific option of requesting a course in other languages. Please check your course to determine foreign language options.

    +
    +
    +
    +

    When will there be more courses on other subjects?

    +
    +

    We are continually reviewing and creating courses to add to the edX platform. Please check the website for future course announcements. You can also "friend" edX on Facebook – you’ll receive updates and announcements.

    +
    +
    +
    +

    How can I help edX?

    +
    +

    You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.

    +
    +
    +
    +

    When does my course start and/or finish?

    +
    +

    You can find the start and stop dates for each course on each course description page.

    +
    +
    +
    +

    Is there a walk-through of a sample course session?

    +
    +

    There are video introductions for every course that will give you a good sense of how the course works and what to expect.

    +
    +
    +
    +

    I don't have the prerequisites for a course that I am interested in. Can I still take the course?

    +
    +

    We do not check students for prerequisites, so you are allowed to attempt the course. However, if you do not know prerequisite subjects before taking a class, you will have to learn the prerequisite material on your own over the semester, which can be an extremely difficult task.

    +
    +
    +
    +

    What happens if I have to quit a course, are there any penalties, will I be able to take another course in the future?

    +
    +

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    +
    +
    +
    -
    -

    Help email

    +
    +

    The Classes

    +
    +

    How much work will I have to do to pass my course?

    +
    +

    The projected hours of study required for each course are described on the specific course description page.

    +
    +
    +
    +

    What should I do before I take a course (prerequisites)?

    +
    +

    Each course is different – some have prerequisites, and some don’t. Take a look at your specific course’s recommended prerequisites. If you do not have a particular prerequisite, you may still take the course.

    +
    +
    +
    +

    What books should I read? (I am interested in reading materials before the class starts).

    +
    +

    Take a look at the specific course prerequisites. All required academic materials will be provided during the course, within the browser. Some of the course descriptions may list additional resources. For supplemental reading material before or during the course, you can post a question on the course’s Discussion Forum to ask your online coursemates for suggestions.

    +
    +
    +
    +

    Can I download the book for my course?

    +
    +

    EdX book content may only be viewed within the browser, and downloading it violates copyright laws. If you need or want a hard copy of the book, we recommend that you purchase a copy.

    +
    +
    +
    +

    Can I take more than one course at a time?

    +
    +

    You may take multiple edX courses, however we recommend checking the requirements on each course description page to determine your available study hours and the demands of the intended courses.

    +
    +
    +
    +

    How do I log in to take an edX class?

    +
    +

    Once you sign up for a course and activate your account, click on the "Log In" button on the edx.org home page. You will need to type in your email address and edX password each time you log in.

    +
    +
    +
    +

    What time is the class?

    +
    +

    EdX classes take place at your convenience. Prefer to sleep in and study late? No worries. Videos and problem sets are available 24 hours a day, which means you can watch video and complete work whenever you have spare time. You simply log in to your course via the Internet and work through the course material, one week at a time.

    +
    +
    +
    +

    If I miss a week, how does this affect my grade?

    +
    +

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    +
    +
    +
    +

    How can I meet/find other students?

    +
    +

    All edX courses have Discussion Forums where you can chat with and help each other within the framework of the Honor Code.

    +
    +
    +
    +

    How can I talk to professors, fellows and teaching assistants?

    +
    +

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    +
    +
    +
    + +
    +

    Getting Help

    +
    +

    Can I re-take a course?

    +
    +

    Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.

    +
    +
    +
    +

    Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?

    +
    +

    Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.

    +
    +
    +
    +

    Is there an exam at the end?

    +
    +

    Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.

    +
    +
    +
    +

    Will the same courses be offered again in the future?

    +
    +

    Existing edX courses will be re-offered, and more courses added.

    +
    +
    +
    +

    Will I get a certificate for taking an edX course?

    +
    +

    Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.

    +
    +
    +
    +

    How are edX certificates delivered?

    +
    +

    EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.

    +
    +
    +
    +

    What is the difference between a proctored certificate and an honor code certificate?

    +
    +

    A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed the edX honor code .

    +
    +
    +
    +

    Yes. The requirements for both certificates can be independently satisfied.

    +
    +

    It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

    +
    +
    +
    +

    Will my grade be shown on my certificate?

    +
    +

    No. Grades are not displayed on either honor code or proctored certificates.

    +
    +
    +
    +

    How can I talk to professors, fellows and teaching assistants?

    +
    +

    The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

    +
    +
    +
    +

    The only certificates distributed with grades by edX were for the initial prototype course.

    +
    +

    You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

    +
    +
    +
    +

    Will my university accept my edX coursework for credit?

    +
    +

    Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.

    +
    +
    +
    +

    I lost my edX certificate – can you resend it to me?

    +
    +

    Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.

    +
    +
    +
    + +
    +

    edX & Open Source

    +
    +

    What’s open source?

    +
    +

    Open source is a philosophy that generally refers to making software freely available for use or modification as users see fit. In exchange for use of the software, users generally add their contributions to the software, making it a public collaboration. The edX platform will be made available as open source code in order to allow world talent to improve and share it on an ongoing basis.

    +
    +
    +
    +

    When/how can I get the open-source platform technology?

    +
    +

    We are still building the edX technology platform and will be making announcements in the future about its availability.

    +
    +
    +
    + +
    +

    Other Help Questions - Account Questions

    +
    +

    My username is taken.

    +
    +

    Now’s your chance to be creative: please try a different, more unique username – for example, try adding a random number to the end.

    +
    +
    +
    +

    Why does my password show on my course login page?

    +
    +

    Oops! This may be because of the way you created your account. For example, you may have mistakenly typed your password into the login box.

    +
    +
    +
    +

    I am having login problems (password/email unrecognized).

    +
    +

    Please check your browser’s settings to make sure that you have the current version of Firefox or Chrome, and then try logging in again. If you find access impossible, you may simply create a new account using an alternate email address – the old, unused account will disappear later.

    +
    +
    +
    +

    I did not receive an activation email.

    +
    +

    If you did not receive an activation email it may be because:

    +
      +
    • There was a typo in your email address.
    • +
    • Your spam filter may have caught the activation email. Please check your spam folder.
    • +
    • You may be using an older browser. We recommend downloading the current version of Firefox or Chrome.
    • +
    • JavaScript is disabled in your browser. Please check your browser settings and confirm that JavaScript is enabled.
    • +
    +

    If you continue to have problems activating your account, we recommend that you try creating a new account. There is no need to do anything about the old account. If it is not activated through the link in the email, it will disappear later.

    +
    +
    +
    +

    Can I delete my account?

    +
    +

    There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.

    +
    +
    +
    +

    I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.

    +
    +

    Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.

    +
    +
    +
    + +
    + + -
    From 1be58c62e3a92256800b276cd3096f677d7ae969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 16 Jan 2013 17:06:01 -0500 Subject: [PATCH 381/541] Update /faq page: change certificate of completion to mastery --- lms/templates/static_templates/faq.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index b0543df264..91ac16e90c 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -39,7 +39,7 @@

    Will certificates be awarded?

    -

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

    +

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

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

    From 9fdb4d092374c72cd9f953cc6daeb45894e420e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 16 Jan 2013 17:08:54 -0500 Subject: [PATCH 382/541] Replace front page video [#154] --- lms/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/index.html b/lms/templates/index.html index e986566dec..d08ba09e61 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -164,7 +164,7 @@ From 59b328f4b2a9769e08be8a2eae935f049782d6dc Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 17 Jan 2013 10:49:57 -0500 Subject: [PATCH 383/541] pearson registration - removed telephone/fax placeholder attributes since they can't represent both domestic and international formatting easily --- lms/templates/test_center_register.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 4e1cece8c9..1b45497ad7 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -246,25 +246,25 @@
  • - +
    - +
    - +
  • - +
    - +
  • From fdd395e0b31f79206d7fbda25196d505c83b8347 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 17 Jan 2013 10:50:58 -0500 Subject: [PATCH 384/541] pearson registration - synced up link text for emailing edx --- lms/templates/test_center_register.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index 1b45497ad7..f6c53c0e89 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -474,7 +474,7 @@

    Questions

    -

    If you have a specific question pertaining to your registration, you may contact exam-help@edx.org.

    +

    If you have a specific question pertaining to your registration, you may contact edX at exam-help@edx.org.

  • From 45bb511e1557360f3490ef07aefad47ddbeeda79 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 17 Jan 2013 12:06:21 -0500 Subject: [PATCH 385/541] remove obsolete test (per Vik) --- common/lib/xmodule/xmodule/tests/test_import.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 90ec112f19..554e89ac74 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -339,19 +339,6 @@ class ImportTestCase(unittest.TestCase): self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml) - def test_selfassessment_import(self): - ''' - Check to see if definition_from_xml in self_assessment_module.py - works properly. Pulls data from the self_assessment directory in the test data directory. - ''' - - modulestore = XMLModuleStore(DATA_DIR, course_dirs=['self_assessment']) - - sa_id = "edX/sa_test/2012_Fall" - location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) - sa_sample = modulestore.get_instance(sa_id, location) - #10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag - self.assertEqual(sa_sample.metadata['attempts'], '10') def test_graphicslidertool_import(self): ''' From 2d55f871eebb83833f194139c9199484dac5153f Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Thu, 17 Jan 2013 15:51:26 -0500 Subject: [PATCH 386/541] While this is a pain for content developer local environment updates it breaks the LMS in EC2 which is vastly more important. We'll jsut have to address helping individual content people with installing MySQL if needed. This is a real dependency in production and I think those belong in here. --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 08cfe57e2e..bc019ab54c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,8 +51,7 @@ pygraphviz==1.1 pil==1.1.7 nltk==2.0.4 dogstatsd-python==0.2.1 -# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs. -# MySQL-python +MySQL-python==1.2.4c1 sphinx==1.1.3 Shapely==1.2.16 ipython==0.13.1 From 66a3e9621bc30b3c9a3dc4ea49110eab5071bafd Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 17 Jan 2013 17:04:28 -0500 Subject: [PATCH 387/541] studio - removed editor type icon and text for all components except for problems --- cms/static/sass/_unit.scss | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 5edce2419c..43acdc1eea 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -51,6 +51,8 @@ } .components { + + > li { position: relative; z-index: 10; @@ -118,6 +120,24 @@ } } + .new-component-templates { + display: none; + padding: 20px; + @include clearfix; + + .cancel-button { + @include white-button; + } + + // specific menu types + &.new-component-problem { + + .ss-icon, .editor-indicator { + display: inline-block; + } + } + } + .new-component-type, .new-component-template { @include clearfix; @@ -177,7 +197,11 @@ position: relative; top: 3px; font-size: 12px; - opacity: 0.1; + opacity: 0.3; + } + + .ss-icon, .editor-indicator { + display: none; } &:hover { @@ -214,16 +238,6 @@ } } - - .new-component-templates { - display: none; - padding: 20px; - @include clearfix; - - .cancel-button { - @include white-button; - } - } } } } From 5b82a68c184db662dcf72d627132422401b79a1b Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 17 Jan 2013 17:06:07 -0500 Subject: [PATCH 388/541] studio - removing extra line/whitespace in sass --- cms/static/sass/_unit.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 43acdc1eea..d8ca1117e9 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -52,7 +52,6 @@ .components { - > li { position: relative; z-index: 10; @@ -137,7 +136,7 @@ } } } - + .new-component-type, .new-component-template { @include clearfix; From eaa6701c6fd3914653b6989455ac8b162bd9e2fa Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 17 Jan 2013 17:13:31 -0500 Subject: [PATCH 389/541] Remove mitxmako dependencies, make rubric and oechild inherit fro object --- .../xmodule/combined_open_ended_module.py | 14 +++++------ .../xmodule/combined_open_ended_rubric.py | 9 +++---- .../lib/xmodule/xmodule/open_ended_module.py | 25 +++++++++---------- common/lib/xmodule/xmodule/openendedchild.py | 4 +-- .../xmodule/xmodule/self_assessment_module.py | 4 +-- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a88acc6ffd..5c8a88d9f7 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -22,8 +22,6 @@ from xmodule.modulestore import Location import self_assessment_module import open_ended_module -from mitxmako.shortcuts import render_to_string - log = logging.getLogger("mitx.courseware") # Set the default number of max attempts. Should be 1 for production @@ -319,7 +317,7 @@ class CombinedOpenEndedModule(XModule): Output: HTML rendered directly via Mako """ context = self.get_context() - html = render_to_string('combined_open_ended.html', context) + html = self.system.render_template('combined_open_ended.html', context) return html def get_html_base(self): @@ -369,17 +367,17 @@ class CombinedOpenEndedModule(XModule): self.static_data, instance_state=task_state) last_response = task.latest_answer() last_score = task.latest_score() - last_post_assessment = task.latest_post_assessment() + last_post_assessment = task.latest_post_assessment(self.system) last_post_feedback = "" if task_type == "openended": - last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) + last_post_assessment = task.latest_post_assessment(self.system, short_feedback=False, join_feedback=False) if isinstance(last_post_assessment, list): eval_list = [] for i in xrange(0, len(last_post_assessment)): - eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) + eval_list.append(task.format_feedback_with_evaluation(self.system, last_post_assessment[i])) last_post_evaluation = "".join(eval_list) else: - last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) + last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment) last_post_assessment = last_post_evaluation last_correctness = task.is_last_response_correct() max_score = task.max_score() @@ -442,7 +440,7 @@ class CombinedOpenEndedModule(XModule): self.update_task_states() response_dict = self.get_last_response(task_number) context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1} - html = render_to_string('combined_open_ended_results.html', context) + html = self.system.render_template('combined_open_ended_results.html', context) return {'html': html, 'success': True} def handle_ajax(self, dispatch, get): diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index 0b2ca1ca2c..e4daf11f1d 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -1,16 +1,15 @@ -from mitxmako.shortcuts import render_to_string import logging from lxml import etree log=logging.getLogger(__name__) -class CombinedOpenEndedRubric: +class CombinedOpenEndedRubric(object): @staticmethod - def render_rubric(rubric_xml): + def render_rubric(rubric_xml, system): try: rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) - html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) + html = system.render_template('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) except: log.exception("Could not parse the rubric.") html = rubric_xml @@ -64,7 +63,7 @@ class CombinedOpenEndedRubric: if has_score: if scorexml.tag != 'score': - raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag)) + raise Exception("[extract_category]mitxmako: expected score tag, got {0} instead".format(scorexml.tag)) for option in optionsxml: if option.tag != "option": diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 11f96c9848..0eaad34bad 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -30,7 +30,6 @@ from xmodule.modulestore import Location from capa.util import * import openendedchild -from mitxmako.shortcuts import render_to_string from numpy import median from datetime import datetime @@ -256,7 +255,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @param system: Modulesystem @return: Boolean True (not useful currently) """ - new_score_msg = self._parse_score_msg(score_msg) + new_score_msg = self._parse_score_msg(score_msg, system) if not new_score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' @@ -370,7 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return u"\n".join([feedback_list_part1, feedback_list_part2]) - def _format_feedback(self, response_items): + def _format_feedback(self, response_items, system): """ Input: Dictionary called feedback. Must contain keys seen below. @@ -382,13 +381,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): rubric_feedback="" feedback = self._convert_longform_feedback_to_html(response_items) if response_items['rubric_scores_complete']==True: - rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml']) + rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'], system) if not response_items['success']: return system.render_template("open_ended_error.html", {'errors': feedback}) - feedback_template = render_to_string("open_ended_feedback.html", { + feedback_template = system.render_template("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, @@ -398,7 +397,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return feedback_template - def _parse_score_msg(self, score_msg, join_feedback=True): + def _parse_score_msg(self, score_msg, system, join_feedback=True): """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, @@ -450,7 +449,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'rubric_scores_complete' : score_result['rubric_scores_complete'], 'rubric_xml' : score_result['rubric_xml'], } - feedback_items.append(self._format_feedback(new_score_result)) + feedback_items.append(self._format_feedback(new_score_result, system)) if join_feedback: feedback = "".join(feedback_items) else: @@ -458,7 +457,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): score = int(median(score_result['score'])) else: #This is for instructor and ML grading - feedback = self._format_feedback(score_result) + feedback = self._format_feedback(score_result, system) score = score_result['score'] self.submission_id = score_result['submission_id'] @@ -466,7 +465,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid': True, 'score': score, 'feedback': feedback} - def latest_post_assessment(self, short_feedback=False, join_feedback=True): + def latest_post_assessment(self, system, short_feedback=False, join_feedback=True): """ Gets the latest feedback, parses, and returns @param short_feedback: If the long feedback is wanted or not @@ -475,7 +474,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not self.history: return "" - feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback) + feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), system, join_feedback=join_feedback) if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' if feedback_dict['valid']: @@ -483,14 +482,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild): json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' - def format_feedback_with_evaluation(self, feedback): + def format_feedback_with_evaluation(self, system, feedback): """ Renders a given html feedback into an evaluation template @param feedback: HTML feedback @return: Rendered html """ context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50} - html = render_to_string('open_ended_evaluation.html', context) + html = system.render_template('open_ended_evaluation.html', context) return html def handle_ajax(self, dispatch, get, system): @@ -582,7 +581,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if self.state != self.INITIAL: latest = self.latest_answer() previous_answer = latest if latest is not None else self.initial_display - post_assessment = self.latest_post_assessment() + post_assessment = self.latest_post_assessment(system) score = self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' else: diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 2ba9528237..88fed61c6d 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -35,7 +35,7 @@ MAX_ATTEMPTS = 1 # Overriden by max_score specified in xml. MAX_SCORE = 1 -class OpenEndedChild(): +class OpenEndedChild(object): """ States: @@ -123,7 +123,7 @@ class OpenEndedChild(): return None return self.history[-1].get('score') - def latest_post_assessment(self): + def latest_post_assessment(self, system): """None if not available""" if not self.history: return "" diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 940b61c557..3d88cb95f6 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -122,7 +122,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.state == self.INITIAL: return '' - rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric) + rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric, system) # we'll render it context = {'rubric': rubric_html, @@ -147,7 +147,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.state == self.DONE: # display the previous hint - latest = self.latest_post_assessment() + latest = self.latest_post_assessment(system) hint = latest if latest is not None else '' else: hint = '' From 18dc59c93ac9f6e2f5d427f07239ac3ef46c82f5 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 17 Jan 2013 17:18:10 -0500 Subject: [PATCH 390/541] Error message fix --- common/lib/xmodule/xmodule/combined_open_ended_rubric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index e4daf11f1d..c2e0297038 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -63,7 +63,7 @@ class CombinedOpenEndedRubric(object): if has_score: if scorexml.tag != 'score': - raise Exception("[extract_category]mitxmako: expected score tag, got {0} instead".format(scorexml.tag)) + raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag)) for option in optionsxml: if option.tag != "option": From ef69b738c99705faac63d0ed780bd32684eaca9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 17 Jan 2013 21:31:05 -0500 Subject: [PATCH 391/541] Sort courses announcement date If there is no announcement date (in the policy metadata) then use a heuristic to sort them. If there is an announcement date, the use it in the heuristic to determine if the course is new or not. --- common/djangoapps/student/views.py | 45 +++++------ common/lib/xmodule/xmodule/course_module.py | 76 ++++++++++++------ .../xmodule/tests/test_course_module.py | 77 +++++++++++++------ lms/djangoapps/courseware/courses.py | 16 ++++ lms/djangoapps/courseware/views.py | 15 ++-- 5 files changed, 149 insertions(+), 80 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 1a9648835e..61b49e6022 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -27,7 +27,7 @@ from bs4 import BeautifulSoup from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt -from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm, +from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm, TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user, @@ -42,7 +42,7 @@ from xmodule.modulestore.django import modulestore #from datetime import date from collections import namedtuple -from courseware.courses import get_courses +from courseware.courses import get_courses, sort_by_announcement from courseware.access import has_access from statsd import statsd @@ -78,10 +78,7 @@ def index(request, extra_context={}, user=None): domain = request.META.get('HTTP_HOST') courses = get_courses(None, domain=domain) - - # Sort courses by how far are they from they start day - key = lambda course: course.days_until_start - courses = sorted(courses, key=key, reverse=True) + courses = sort_by_announcement(courses) # Get the 3 most recent news top_news = _get_news(top=3) @@ -211,7 +208,7 @@ def _cert_info(user, course, cert_status): def dashboard(request): user = request.user enrollments = CourseEnrollment.objects.filter(user=user) - + # Build our courses list for the user, but ignore any courses that no longer # exist (because the course IDs have changed). Still, we don't delete those # enrollments, because it could have been a data push snafu. @@ -473,7 +470,7 @@ def _do_create_account(post_vars): except (ValueError, KeyError): # If they give us garbage, just ignore it instead # of asking them to put an integer. - profile.year_of_birth = None + profile.year_of_birth = None try: profile.save() except Exception: @@ -613,7 +610,7 @@ def exam_registration_info(user, course): exam_info = course.current_test_center_exam if exam_info is None: return None - + exam_code = exam_info.exam_series_code registrations = get_testcenter_registration(user, course.id, exam_code) if registrations: @@ -621,7 +618,7 @@ def exam_registration_info(user, course): else: registration = None return registration - + @login_required @ensure_csrf_cookie def begin_exam_registration(request, course_id): @@ -647,7 +644,7 @@ def begin_exam_registration(request, course_id): # determine if the user is registered for this course: registration = exam_registration_info(user, course) - + # we want to populate the registration page with the relevant information, # if it already exists. Create an empty object otherwise. try: @@ -655,7 +652,7 @@ def begin_exam_registration(request, course_id): except TestCenterUser.DoesNotExist: testcenteruser = TestCenterUser() testcenteruser.user = user - + context = {'course': course, 'user': user, 'testcenteruser': testcenteruser, @@ -672,8 +669,8 @@ def create_exam_registration(request, post_override=None): Called by form in test_center_register.html ''' post_vars = post_override if post_override else request.POST - - # first determine if we need to create a new TestCenterUser, or if we are making any update + + # first determine if we need to create a new TestCenterUser, or if we are making any update # to an existing TestCenterUser. username = post_vars['username'] user = User.objects.get(username=username) @@ -686,10 +683,10 @@ def create_exam_registration(request, post_override=None): for fieldname in TestCenterUser.user_provided_fields(): if fieldname in post_vars: demographic_data[fieldname] = (post_vars[fieldname]).strip() - + try: testcenter_user = TestCenterUser.objects.get(user=user) - needs_updating = testcenter_user.needs_update(demographic_data) + needs_updating = testcenter_user.needs_update(demographic_data) log.info("User {0} enrolled in course {1} {2}updating demographic info for exam registration".format(user.username, course_id, "" if needs_updating else "not ")) except TestCenterUser.DoesNotExist: # do additional initialization here: @@ -699,7 +696,7 @@ def create_exam_registration(request, post_override=None): # perform validation: if needs_updating: - # first perform validation on the user information + # first perform validation on the user information # using a Django Form. form = TestCenterUserForm(instance=testcenter_user, data=demographic_data) if form.is_valid(): @@ -710,7 +707,7 @@ def create_exam_registration(request, post_override=None): response_data['field_errors'] = form.errors response_data['non_field_errors'] = form.non_field_errors() return HttpResponse(json.dumps(response_data), mimetype="application/json") - + # create and save the registration: needs_saving = False exam = course.current_test_center_exam @@ -720,12 +717,12 @@ def create_exam_registration(request, post_override=None): registration = registrations[0] # NOTE: we do not bother to check here to see if the registration has changed, # because at the moment there is no way for a user to change anything about their - # registration. They only provide an optional accommodation request once, and + # registration. They only provide an optional accommodation request once, and # cannot make changes to it thereafter. # It is possible that the exam_info content has been changed, such as the # scheduled exam dates, but those kinds of changes should not be handled through - # this registration screen. - + # this registration screen. + else: accommodation_request = post_vars.get('accommodation_request','') registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request) @@ -733,7 +730,7 @@ def create_exam_registration(request, post_override=None): log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id)) if needs_saving: - # do validation of registration. (Mainly whether an accommodation request is too long.) + # do validation of registration. (Mainly whether an accommodation request is too long.) form = TestCenterRegistrationForm(instance=registration, data=post_vars) if form.is_valid(): form.update_and_save() @@ -743,14 +740,14 @@ def create_exam_registration(request, post_override=None): response_data['field_errors'] = form.errors response_data['non_field_errors'] = form.non_field_errors() return HttpResponse(json.dumps(response_data), mimetype="application/json") - + # only do the following if there is accommodation text to send, # and a destination to which to send it. # TODO: still need to create the accommodation email templates # if 'accommodation_request' in post_vars and 'TESTCENTER_ACCOMMODATION_REQUEST_EMAIL' in settings: # d = {'accommodation_request': post_vars['accommodation_request'] } -# +# # # composes accommodation email # subject = render_to_string('emails/accommodation_email_subject.txt', d) # # Email subject *must not* contain newlines diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 499247cc2d..bc171ca5b9 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,4 +1,5 @@ import logging +from math import exp, erf from lxml import etree from path import path # NOTE (THK): Only used for detecting presence of syllabus import requests @@ -183,35 +184,66 @@ class CourseDescriptor(SequenceDescriptor): @property def is_new(self): - # The course is "new" if either if the metadata flag is_new is - # true or if the course has not started yet + """ + Returns if the course has been flagged as new in the metadata. If + there is no flag, return a heuristic value considering the + announcement and the start dates. + """ flag = self.metadata.get('is_new', None) if flag is None: - return self.days_until_start > 1 + # Use a heuristic if the course has not been flagged + announcement, start, now = self._sorting_dates() + if announcement and (now - announcement).days < 30: + # The course has been announced for less that month + return True + elif (now - start).days < 1: + # The course has not started yet + return True + else: + return False elif isinstance(flag, basestring): return flag.lower() in ['true', 'yes', 'y'] else: return bool(flag) @property - def days_until_start(self): - def convert_to_datetime(timestamp): + def sorting_score(self): + """ + Returns a number that can be used to sort the courses according + the how "new"" they are. The "newness"" score is computed using a + heuristic that takes into account the announcement and + (advertized) start dates of the course if available. + + The lower the number the "newer" the course. + """ + # Make courses that have an announcement date shave a lower + # score than courses than don't, older courses should have a + # higher score. + announcement, start, now = self._sorting_dates() + scale = 300.0 # about a year + if announcement: + days = (now - announcement).days + score = -exp(-days/scale) + else: + days = (now - start).days + score = exp(days/scale) + return score + + def _sorting_dates(self): + # utility function to get datetime objects for dates used to + # compute the is_new flag and the sorting_score + def to_datetime(timestamp): return datetime.fromtimestamp(time.mktime(timestamp)) - start_date = convert_to_datetime(self.start) + def get_date(field): + timetuple = self._try_parse_time(field) + return to_datetime(timetuple) if timetuple else None - # Try to use course advertised date if we can parse it - advertised_start = self.metadata.get('advertised_start', None) - if advertised_start: - try: - start_date = datetime.strptime(advertised_start, - "%Y-%m-%dT%H:%M") - except ValueError: - pass # Invalid date, keep using 'start'' + announcement = get_date('announcement') + start = get_date('advertised_start') or to_datetime(self.start) + now = to_datetime(time.gmtime()) - now = convert_to_datetime(time.gmtime()) - days_until_start = (start_date - now).days - return days_until_start + return announcement, start, now @lazyproperty def grading_context(self): @@ -387,9 +419,9 @@ class CourseDescriptor(SequenceDescriptor): self.first_eligible_appointment_date = self._try_parse_time('First_Eligible_Appointment_Date') if self.first_eligible_appointment_date is None: raise ValueError("First appointment date must be specified") - # TODO: If defaulting the last appointment date, it should be the + # TODO: If defaulting the last appointment date, it should be the # *end* of the same day, not the same time. It's going to be used as the - # end of the exam overall, so we don't want the exam to disappear too soon. + # end of the exam overall, so we don't want the exam to disappear too soon. # It's also used optionally as the registration end date, so time matters there too. self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date if self.last_eligible_appointment_date is None: @@ -403,7 +435,7 @@ class CourseDescriptor(SequenceDescriptor): raise ValueError("First appointment date must be before last appointment date") if self.registration_end_date > self.last_eligible_appointment_date: raise ValueError("Registration end date must be before last appointment date") - + def _try_parse_time(self, key): """ @@ -434,7 +466,7 @@ class CourseDescriptor(SequenceDescriptor): def is_registering(self): now = time.gmtime() return now >= self.registration_start_date and now <= self.registration_end_date - + @property def first_eligible_appointment_date_text(self): return time.strftime("%b %d, %Y", self.first_eligible_appointment_date) @@ -451,7 +483,7 @@ class CourseDescriptor(SequenceDescriptor): def current_test_center_exam(self): exams = [exam for exam in self.test_center_exams if exam.has_started_registration() and not exam.has_ended()] if len(exams) > 1: - # TODO: output some kind of warning. This should already be + # TODO: output some kind of warning. This should already be # caught if we decide to do validation at load time. return exams[0] elif len(exams) == 1: diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 63eaec1f61..712b095696 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -1,5 +1,5 @@ import unittest -from time import strptime, gmtime +from time import strptime from fs.memoryfs import MemoryFS from mock import Mock, patch @@ -39,52 +39,81 @@ class DummySystem(ImportSystem): class IsNewCourseTestCase(unittest.TestCase): """Make sure the property is_new works on courses""" @staticmethod - def get_dummy_course(start, is_new=None, load_error_modules=True): + def get_dummy_course(start, announcement=None, is_new=None): """Get a dummy course""" - system = DummySystem(load_error_modules) - is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower() + system = DummySystem(load_error_modules=True) + + def to_attrb(n, v): + return '' if v is None else '{0}="{1}"'.format(n, v).lower() + + is_new = to_attrb('is_new', is_new) + announcement = to_attrb('announcement', announcement) start_xml = ''' Two houses, ... - '''.format(org=ORG, course=COURSE, start=start, is_new=is_new) + '''.format(org=ORG, course=COURSE, start=start, is_new=is_new, + announcement=announcement) return system.process_xml(start_xml) @patch('xmodule.course_module.time.gmtime') - def test_non_started_yet(self, gmtime_mock): - descriptor = self.get_dummy_course(start='2013-01-05T12:00') + def test_sorting_score(self, gmtime_mock): gmtime_mock.return_value = NOW - assert(descriptor.is_new == True) - assert(descriptor.days_until_start == 4) + dates = [('2012-10-01T12:00', '2012-09-01T12:00'), # 0 + ('2012-12-01T12:00', '2012-11-01T12:00'), # 1 + ('2013-02-01T12:00', '2012-12-01T12:00'), # 2 + ('2013-02-01T12:00', '2012-11-10T12:00'), # 3 + ('2013-02-01T12:00', None), # 4 + ('2013-03-01T12:00', None), # 5 + ('2013-04-01T12:00', None), # 6 + ('2012-11-01T12:00', None), # 7 + ('2012-09-01T12:00', None), # 8 + ('1990-01-01T12:00', None), # 9 + ('2013-01-02T12:00', None), # 10 + ('2013-01-10T12:00', '2012-12-31T12:00'), # 11 + ('2013-01-10T12:00', '2013-01-01T12:00'), # 12 + ] + + data = [] + for i, d in enumerate(dates): + descriptor = self.get_dummy_course(start=d[0], announcement=d[1]) + score = descriptor.sorting_score + data.append((score, i)) + + result = [d[1] for d in sorted(data)] + assert(result == [12, 11, 2, 3, 1, 0, 6, 5, 4, 10, 7, 8, 9]) + @patch('xmodule.course_module.time.gmtime') - def test_already_started(self, gmtime_mock): - gmtime_mock.return_value = NOW - - descriptor = self.get_dummy_course(start='2012-12-02T12:00') - assert(descriptor.is_new == False) - assert(descriptor.days_until_start < 0) - - @patch('xmodule.course_module.time.gmtime') - def test_is_new_set(self, gmtime_mock): + def test_is_new(self, gmtime_mock): gmtime_mock.return_value = NOW descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) - assert(descriptor.is_new == True) - assert(descriptor.days_until_start < 0) + assert(descriptor.is_new is True) descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False) - assert(descriptor.is_new == False) - assert(descriptor.days_until_start > 0) + assert(descriptor.is_new is False) descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True) - assert(descriptor.is_new == True) - assert(descriptor.days_until_start > 0) + assert(descriptor.is_new is True) + + descriptor = self.get_dummy_course(start='2013-01-15T12:00') + assert(descriptor.is_new is True) + + descriptor = self.get_dummy_course(start='2013-03-00T12:00') + assert(descriptor.is_new is True) + + descriptor = self.get_dummy_course(start='2012-10-15T12:00') + assert(descriptor.is_new is False) + + descriptor = self.get_dummy_course(start='2012-12-31T12:00') + assert(descriptor.is_new is True) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 7c0d30ebd8..1090c208d1 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -64,6 +64,7 @@ def course_image_url(course): path = course.metadata['data_dir'] + "/images/course_image.jpg" return try_staticfiles_lookup(path) + def find_file(fs, dirs, filename): """ Looks for a filename in a list of dirs on a filesystem, in the specified order. @@ -80,6 +81,7 @@ def find_file(fs, dirs, filename): return filepath raise ResourceNotFoundError("Could not find {0}".format(filename)) + def get_course_about_section(course, section_key): """ This returns the snippet of html to be rendered on the course about page, @@ -234,4 +236,18 @@ def get_courses(user, domain=None): courses = [c for c in courses if has_access(user, c, 'see_exists')] courses = sorted(courses, key=lambda course:course.number) + + return courses + + +def sort_by_announcement(courses): + """ + Sorts a list of courses by their announcement date. If the date is + not available, sort them by their start date. + """ + + # Sort courses by how far are they from they start day + key = lambda course: course.sorting_score + courses = sorted(courses, key=key) + return courses diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 9e52e2b281..b3775eb663 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -17,7 +17,8 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access -from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university) +from courseware.courses import (get_courses, get_course_with_access, + get_courses_by_university, sort_by_announcement) import courseware.tabs as tabs from courseware.models import StudentModuleCache from module_render import toc_for_course, get_module, get_instance_module @@ -67,11 +68,8 @@ def courses(request): ''' Render "find courses" page. The course selection work is done in courseware.courses. ''' - courses = get_courses(request.user, domain=request.META.get('HTTP_HOST')) - - # Sort courses by how far are they from they start day - key = lambda course: course.days_until_start - courses = sorted(courses, key=key, reverse=True) + courses = get_courses(request.user, request.META.get('HTTP_HOST')) + courses = sort_by_announcement(courses) return render_to_response("courseware/courses.html", {'courses': courses}) @@ -438,10 +436,7 @@ def university_profile(request, org_id): # Only grab courses for this org... courses = get_courses_by_university(request.user, domain=request.META.get('HTTP_HOST'))[org_id] - - # Sort courses by how far are they from they start day - key = lambda course: course.days_until_start - courses = sorted(courses, key=key, reverse=True) + courses = sort_by_announcement(courses) context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() From c6276c8394a53f78220bf985e340777f5bcde449 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 17 Jan 2013 23:58:07 -0500 Subject: [PATCH 392/541] only cache discussion forum info for 5 minutes before flushing. This is needed for Studio backed courses that can add new discussion items at any point in time. Also we need to also set metadata fields (which are basically the same as what gets passed in the XML) as the forums expect those values as metadata --- .../xmodule/templates/discussion/default.yaml | 3 +++ lms/djangoapps/django_comment_client/utils.py | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml index fa6dcbff54..d34e6378e6 100644 --- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml +++ b/common/lib/xmodule/xmodule/templates/discussion/default.yaml @@ -1,6 +1,9 @@ --- metadata: display_name: Discussion Tag + for: Topic-Level Student-Visible Label + id: 6002x_group_discussion_by_this + discussion_category: Week 1 data: | children: [] diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 1f39ebfcfd..3094367491 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -2,6 +2,7 @@ from collections import defaultdict import logging import time import urllib +from datetime import datetime from django.contrib.auth.models import User from django.core.urlresolvers import reverse @@ -63,22 +64,19 @@ def get_discussion_id_map(course): return a dict of the form {category: modules} """ global _DISCUSSIONINFO - if not _DISCUSSIONINFO[course.id]: - initialize_discussion_info(course) + initialize_discussion_info(course) return _DISCUSSIONINFO[course.id]['id_map'] def get_discussion_title(course, discussion_id): global _DISCUSSIONINFO - if not _DISCUSSIONINFO[course.id]: - initialize_discussion_info(course) + initialize_discussion_info(course) title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)') return title def get_discussion_category_map(course): global _DISCUSSIONINFO - if not _DISCUSSIONINFO[course.id]: - initialize_discussion_info(course) + initialize_discussion_info(course) return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map']) def filter_unstarted_categories(category_map): @@ -131,8 +129,18 @@ def sort_map_entries(category_map): def initialize_discussion_info(course): global _DISCUSSIONINFO + + # only cache in-memory discussion information for 10 minutes + # this is because we need a short-term hack fix for + # mongo-backed courseware whereby new discussion modules can be added + # without LMS service restart + if _DISCUSSIONINFO[course.id]: - return + timestamp = _DISCUSSIONINFO[course.id].get('timestamp', datetime.now()) + age = datetime.now() - timestamp + # expire every 5 minutes + if age.seconds < 300: + return course_id = course.id @@ -209,6 +217,7 @@ def initialize_discussion_info(course): _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['category_map'] = category_map + _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() class JsonResponse(HttpResponse): def __init__(self, data=None): From aeee13ab9127f1b35c846911a048ae9ae1773c8d Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Fri, 18 Jan 2013 10:01:21 -0500 Subject: [PATCH 393/541] Fix the virtualenv creation for Jenkins. --- jenkins/test.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jenkins/test.sh b/jenkins/test.sh index a354039359..cd8800b676 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -27,7 +27,12 @@ git submodule foreach 'git reset --hard HEAD' export PYTHONIOENCODING=UTF-8 GIT_BRANCH=${GIT_BRANCH/HEAD/master} +if [ ! -d /mnt/virtualenvs/"$JOB_NAME" ]; then + mkdir -p /mnt/virtualenvs/"$JOB_NAME" + virtualenv /mnt/virtualenvs/"$JOB_NAME" +fi +source /mnt/virtualenvs/"$JOB_NAME"/bin/activate pip install -q -r pre-requirements.txt pip install -q -r test-requirements.txt yes w | pip install -q -r requirements.txt From 72fc192f6ef75f44b5e1d29dbbec8993d7b16474 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 18 Jan 2013 10:24:29 -0500 Subject: [PATCH 394/541] studio - adding in LMS synced styles for TinyMCE WYSIWYG preview --- cms/static/css/tiny-mce.css | 115 ++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 38 deletions(-) diff --git a/cms/static/css/tiny-mce.css b/cms/static/css/tiny-mce.css index 7f32e59c8f..63d2bada94 100644 --- a/cms/static/css/tiny-mce.css +++ b/cms/static/css/tiny-mce.css @@ -1,50 +1,89 @@ -body { - background-color: #fff; - font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif; - font-size: 16px; - line-height: 1.6; - color: #3c3c3c; - scrollbar-3dlight-color: #F0F0EE; - scrollbar-arrow-color: #676662; - scrollbar-base-color: #F0F0EE; - scrollbar-darkshadow-color: #DDDDDD; - scrollbar-face-color: #E0E0DD; - scrollbar-highlight-color: #F0F0EE; - scrollbar-shadow-color: #F0F0EE; - scrollbar-track-color: #F5F5F5; +.mceContentBody { + padding: 10px; + background-color: #fff; + font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.6; + color: #3c3c3c; + scrollbar-3dlight-color: #F0F0EE; + scrollbar-arrow-color: #676662; + scrollbar-base-color: #F0F0EE; + scrollbar-darkshadow-color: #DDDDDD; + scrollbar-face-color: #E0E0DD; + scrollbar-highlight-color: #F0F0EE; + scrollbar-shadow-color: #F0F0EE; + scrollbar-track-color: #F5F5F5; } -a { - color: #1d9dd9; - text-decoration: none; -} - -p { - font-size: 16px; - line-height: 1.6; +h1 { + color: #3c3c3c; + font-weight: normal; + font-size: 2em; + line-height: 1.4em; + letter-spacing: 1px; } h2 { - color: #646464; - font-size: 19px; - font-weight: 300; - letter-spacing: 1px; - margin-bottom: 15px; - text-transform: uppercase; + color: #646464; + font-weight: normal; + font-size: 1.2em; + line-height: 1.2em; + letter-spacing: 1px; + margin-bottom: 15px; + text-transform: uppercase; + -webkit-font-smoothing: antialiased; } h3 { - font-size: 19px; - font-weight: 400; + font-size: 1.2em; + font-weight: 600; +} + +p { + margin-bottom: 1.416em; + font-size: 1em; + line-height: 1.6em !important; + color: $baseFontColor; +} + +em, i { + font-style: italic; +} + +strong, b { + font-style: bold; +} + +p + p, ul + p, ol + p { + margin-top: 20px; +} + +ol, ul { + margin: 1em 0; + padding: 0 0 0 1em; +} + +ol li, ul li { + margin-bottom: 0.708em; +} + +ol { + list-style: decimal outside none; +} + +ul { + list-style: disc outside none; +} + +a, a:link, a:visited, a:hover, a:active { + color: #1d9dd9; +} + +img { + max-width: 100%; } code { - margin: 0 2px; - padding: 0px 5px; - border-radius: 3px; - border: 1px solid #eaeaea; - white-space: nowrap; - font-family: Monaco, monospace; - font-size: 14px; - background-color: #f8f8f8; + font-family: monospace, serif; + background: none; } \ No newline at end of file From 700b0f14fd72c4b491854e0d51469efbdea573e2 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 18 Jan 2013 10:48:42 -0500 Subject: [PATCH 395/541] Need path to tiny mce JS file now. --- common/lib/xmodule/jasmine_test_runner.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/jasmine_test_runner.html.erb b/common/lib/xmodule/jasmine_test_runner.html.erb index 5ee06b5f1b..3327ab4aea 100644 --- a/common/lib/xmodule/jasmine_test_runner.html.erb +++ b/common/lib/xmodule/jasmine_test_runner.html.erb @@ -16,6 +16,7 @@ + + +
    +
    +

    Afraid of physics? Do you hate it?
    Walter Lewin will make you love physics whether you like it or not

    +
    +
    +

    MIT physics professor and online web star brings his renowned Electricity and Magnetism course to edX

    + +
    + +
    +

    Walter Lewin, legendary MIT physics professor, demonstrates, in his inimitable fashion, one of the many laws of physics covered in his new course on edX.

    +

    Credit: Dominick Reuter

    + High Resolution Image

    +
    +
    + +

    CAMBRIDGE, MA – January 22, 2013 – EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today a new course from the legendary Professor Walter Lewin who, for 47 years, has provided generations of MIT students – and millions watching online – with his inspiring and unconventional lectures. Now, with this edX version of Professor Lewin’s famous course Electricity and Magnetism (Physics), people around the world can experience it just like his students on the MIT campus. MITx 8.02x Electricity and Magnetism is now open for enrollment and classes will begin on February 18, 2013.

    + +

    “I have taught this course to tens of thousands and many tell me it changed their lives,” said Walter Lewin, Professor of Physics at MIT. “Teaching is my passion: I want to open peoples’ eyes and minds to the beauty of physics so they will begin to see the world in a new way.”

    + +

    In 8.02x Electricity and Magnetism, Professor Lewin will teach students to “see” the world instead of just “looking at” it. He will make them “see” natural phenomena such as rainbows in a way they never imagined before. Through his dynamic teaching, enthusiasm and great sense of humor, Professor Lewin has an innate ability to make difficult concepts easy. The New York Times has crowned him a “Web Star” and noted how his lectures, with their engaging physics demonstrations, have won him devotees around the world. While this course is MIT level, edX and Professor Lewin encourage even senior high school students from around the world to watch his lectures and take the course.

    + +

    “Walter Lewin is an international treasure,” said Anant Agarwal, President of edX. “His physics lectures on the MIT campus were already legendary before he put them online and they became an international sensation. We know edX learners will be awestruck by his provocative and enlightening course.”

    + +

    In addition to the basic concepts of Electromagnetism, a vast variety of interesting topics are covered, including Lightning, Pacemakers, Electric Shock Treatment, Electrocardiograms, Metal Detectors, Musical Instruments, Magnetic Levitation, Bullet Trains, Electric Motors, Radios, TV, Car Coils, Superconductivity, Aurora Borealis, Rainbows, Radio Telescopes, Interferometers, Particle Accelerators such as the Large Hadron Collider, Mass Spectrometers, Red Sunsets, Blue Skies, Haloes around Sun and Moon, Color Perception, Doppler Effect and Big-Bang Cosmology.

    + +

    Professor Lewin received his PhD in Nuclear Physics at the Technical University in Delft, the Netherlands in 1965. He joined the Physics faculty at MIT in 1966 and became a pioneer in the new field of X-ray Astronomy. His 105 online lectures are world-renowned and are viewed by nearly 2 million people annually. Professor Lewin has received five teaching awards and is the only MIT professor featured in "The Best 300 Professors" by The Princeton Review. He has co-authored with Warren Goldstein the book "For the Love of Physics" (Free Press, Simon & Schuster), which has been translated into 9 languages.

    + +

    Previously announced new 2013 courses include: Justice from Michael Sandel; Introduction to Statistics from Ani Adhikari; The Challenges of Global Poverty from Esther Duflo; The Ancient Greek Hero from Gregory Nagy; Quantum Mechanics and Quantum Computation from Umesh Vazirani; Human Health and Global Environmental Change, from Aaron Bernstein and Jack Spengler.

    + +

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

    + +

    About edX

    + +

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

    + +
    +

    Contact:

    +

    Brad Baker, Weber Shandwick for edX

    +

    BBaker@webershandwick.com

    +

    (617) 520-7043

    +
    + + +
    +
    +
    diff --git a/lms/urls.py b/lms/urls.py index cab0533f89..ad3b324534 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -118,9 +118,11 @@ urlpatterns = ('', {'template': 'press_releases/Georgetown_joins_edX.html'}, name="press/georgetown-joins-edx"), url(r'^press/spring-courses$', 'static_template_view.views.render', {'template': 'press_releases/Spring_2013_course_announcements.html'}, name="press/spring-courses"), + url(r'^press/lewin-course-announcement$', 'static_template_view.views.render', + {'template': 'press_releases/Lewin_course_announcement.html'}, name="press/lewin-course-announcement"), # Should this always update to point to the latest press release? - (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/spring-courses'}), + (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/lewin-course-announcement'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), From 183721ecf5247e00d0dc8e0cc903509a3b907760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 21 Jan 2013 13:59:05 -0500 Subject: [PATCH 538/541] Correct last screenshot caption of media-kit page. [#159] --- lms/templates/static_templates/media-kit.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/templates/static_templates/media-kit.html b/lms/templates/static_templates/media-kit.html index 458cfb8e15..73eea9c3b8 100644 --- a/lms/templates/static_templates/media-kit.html +++ b/lms/templates/static_templates/media-kit.html @@ -89,7 +89,7 @@
    -
    Screenshot of 6.00x: Introduction to Computer Science and Programming.
    +
    Screenshot of 3.091x: Introduction to Solid State Chemistry.
    Download (High Resolution Photo)
    @@ -108,4 +108,4 @@ return false; }); - \ No newline at end of file + From 45f2a690d039276b797d503f6f37ee3250c81a47 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 17 Jan 2013 06:51:57 -0700 Subject: [PATCH 539/541] Commented on high level functions --- lms/lib/symmath/formula.py | 16 ++++++++++---- lms/lib/symmath/symmath_check.py | 37 +++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index bab0ab3691..c34156da52 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -154,8 +154,9 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False class formula(object): ''' - Representation of a mathematical formula object. Accepts mathml math expression for constructing, - and can produce sympy translation. The formula may or may not include an assignment (=). + Representation of a mathematical formula object. Accepts mathml math expression + for constructing, and can produce sympy translation. The formula may or may not + include an assignment (=). ''' def __init__(self, expr, asciimath='', options=None): self.expr = expr.strip() @@ -194,8 +195,12 @@ class formula(object): def preprocess_pmathml(self, xml): ''' - Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also - to accomodate some sympy conventions (eg hat(i) for \hat{i}). + Pre-process presentation MathML from ASCIIMathML to make it more + acceptable for SnuggleTeX, and also to accomodate some sympy + conventions (eg hat(i) for \hat{i}). + + This method would be a good spot to look for an integral and convert + it, if possible... ''' if type(xml) == str or type(xml) == unicode: @@ -266,6 +271,9 @@ class formula(object): ''' Return sympy expression for the math formula. The math formula is converted to Content MathML then that is parsed. + + This is a recursive function, called on every CMML node. Support for + more functions can be added by modifying opdict, abould halfway down ''' if self.the_sympy: return self.the_sympy diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py index bcb4a0d490..3cc4fd7d3c 100644 --- a/lms/lib/symmath/symmath_check.py +++ b/lms/lib/symmath/symmath_check.py @@ -157,13 +157,33 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None ''' Check a symbolic mathematical expression using sympy. The input may be presentation MathML. Uses formula. + + This is the default Symbolic Response checking function + + Desc of args: + expect is a sympy string representing the correct answer. It is interpreted + using my_sympify (from formula.py), which reads strings as sympy input + (e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5) + + ans is student-typed answer. It is expected to be ascii math, but the code + below would support a sympy string. + + dynamath is the PMathML string converted by MathJax. It is used if + evaluation with ans is not sufficient. + + options is a string with these possible substrings, set as an xml property + of the problem: + -matrix - make a sympy matrix, rather than a list of lists, if possible + -qubit - passed to my_sympify + -imaginary - used in formla, presumably to signal to use i as sqrt(-1)? + -numerical - force numerical comparison. ''' msg = '' # msg += '

    abname=%s' % abname # msg += '

    adict=%s' % (repr(adict).replace('<','<')) - threshold = 1.0e-3 + threshold = 1.0e-3 # for numerical comparison (also with matrices) DEBUG = debug if xml is not None: @@ -184,13 +204,17 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None msg += '

    Error %s in parsing OUR expected answer "%s"

    ' % (err, expect) return {'ok': False, 'msg': make_error_message(msg)} + + ###### Sympy input ####### # if expected answer is a number, try parsing provided answer as a number also try: fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit) except Exception, err: fans = None - if hasattr(fexpect, 'is_number') and fexpect.is_number and fans and hasattr(fans, 'is_number') and fans.is_number: + # do a numerical comparison if both expected and answer are numbers + if (hasattr(fexpect, 'is_number') and fexpect.is_number and fans + and hasattr(fans, 'is_number') and fans.is_number): if abs(abs(fans - fexpect) / fexpect) < threshold: return {'ok': True, 'msg': msg} else: @@ -208,6 +232,8 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None msg += '

    You entered: %s

    ' % to_latex(fans) return {'ok': True, 'msg': msg} + + ###### PMathML input ###### # convert mathml answer to formula try: if dynamath: @@ -216,6 +242,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None mmlans = None if not mmlans: return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath} + f = formula(mmlans, options=options) # get sympy representation of the formula @@ -238,7 +265,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None msg += '
    ' return {'ok': False, 'msg': make_error_message(msg)} - # compare with expected + # do numerical comparison with expected if hasattr(fexpect, 'is_number') and fexpect.is_number: if hasattr(fsym, 'is_number') and fsym.is_number: if abs(abs(fsym - fexpect) / fexpect) < threshold: @@ -250,6 +277,10 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None # msg += "

    cmathml =

    %s

    " % str(f.cmathml).replace('<','<') return {'ok': False, 'msg': make_error_message(msg)} + # Here is a good spot for adding calls to X.simplify() or X.expand(), + # allowing equivalence over binomial expansion or trig identities + + # exactly the same? if fexpect == fsym: return {'ok': True, 'msg': msg} From edda80d485883c34038a1e6adb79666719314e2f Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 22 Jan 2013 11:30:24 -0500 Subject: [PATCH 540/541] New style base python classes and basestring type. --- cms/djangoapps/models/settings/course_details.py | 5 ++--- cms/djangoapps/models/settings/course_grading.py | 2 +- common/djangoapps/models/course_relative.py | 2 +- common/djangoapps/util/converters.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 2bb9d98be7..d01e784d74 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -12,7 +12,7 @@ import re import logging -class CourseDetails: +class CourseDetails(object): def __init__(self, location): self.course_location = location # a Location obj self.start_date = None # 'start' @@ -79,8 +79,7 @@ class CourseDetails: descriptor = get_modulestore(course_location).get_item(course_location) dirty = False - - ## ??? Will this comparison work? + if 'start_date' in jsondict: converted = jsdate_to_time(jsondict['start_date']) else: diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index e0bab1f225..9cfa18c8c9 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -4,7 +4,7 @@ import re from util import converters -class CourseGradingModel: +class CourseGradingModel(object): """ Basically a DAO and Model combo for CRUD operations pertaining to grading policy. """ diff --git a/common/djangoapps/models/course_relative.py b/common/djangoapps/models/course_relative.py index 4dfb83d183..58cc0fb0de 100644 --- a/common/djangoapps/models/course_relative.py +++ b/common/djangoapps/models/course_relative.py @@ -1,4 +1,4 @@ -class CourseRelativeMember: +class CourseRelativeMember(object): def __init__(self, location, idx): self.course_location = location # a Location obj self.idx = idx # which milestone this represents. Hopefully persisted # so we don't have race conditions diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py index 17c45114d1..7f96dc6c30 100644 --- a/common/djangoapps/util/converters.py +++ b/common/djangoapps/util/converters.py @@ -15,7 +15,7 @@ def jsdate_to_time(field): """ if field is None: return field - elif isinstance(field, unicode) or isinstance(field, str): # iso format but ignores time zone assuming it's Z + elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable return d.utctimetuple() elif isinstance(field, int) or isinstance(field, float): From bbcab7314a07ec2d34347935a7808e4e9f8a297f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 22 Jan 2013 14:10:01 -0500 Subject: [PATCH 541/541] Anonymous users don't have an email attribute, and shouldn't be checked for CourseEnrollmentAllowed --- lms/djangoapps/courseware/access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index a176d2a171..f58c53c0cc 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -127,7 +127,7 @@ def _has_access_course_desc(user, course, action): return True # if user is in CourseEnrollmentAllowed with right course_id then can also enroll - if user is not None and CourseEnrollmentAllowed: + if user is not None and user.is_authenticated() and CourseEnrollmentAllowed: if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): return True