From d9b553b378fcd38b88e08c6248dc2b3ec13bcd47 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 5 Nov 2012 15:29:49 -0800 Subject: [PATCH 001/133] Adding a better command for assigning roles. --- .../commands/assign_roles_for_course.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py diff --git a/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py new file mode 100644 index 0000000000..82f2290bc7 --- /dev/null +++ b/lms/djangoapps/django_comment_client/management/commands/assign_roles_for_course.py @@ -0,0 +1,27 @@ +""" +This must be run only after seed_permissions_roles.py! + +Creates default roles for all users in the provided course. Just runs through +Enrollments. +""" +from django.core.management.base import BaseCommand, CommandError + +from student.models import CourseEnrollment, assign_default_role + +class Command(BaseCommand): + args = 'course_id' + help = 'Add roles for all users in a course' + + def handle(self, *args, **options): + if len(args) == 0: + raise CommandError("Please provide a course id") + if len(args) > 1: + raise CommandError("Too many arguments") + course_id = args[0] + + print "Updated roles for ", + for i, enrollment in enumerate(CourseEnrollment.objects.filter(course_id=course_id), start=1): + assign_default_role(None, enrollment) + if i % 1000 == 0: + print "{0}...".format(i), + print From 10c623d1335507ba9c0f54f4ba03cd370666fb40 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 14 Nov 2012 21:41:17 -0500 Subject: [PATCH 002/133] Django commands for certificates * Removing the command to find unicode names since it was only used for testing * ungenerated_certs will now by default run through all courses that have ended. a course option can be used to run on one course * gen_cert_report added which will summarize certificate states for all courses that have ended --- .../management/commands/find_unicode_certs.py | 38 -------- .../management/commands/gen_cert_report.py | 95 +++++++++++++++++++ .../management/commands/ungenerated_certs.py | 93 +++++++++++++++--- 3 files changed, 173 insertions(+), 53 deletions(-) delete mode 100644 lms/djangoapps/certificates/management/commands/find_unicode_certs.py create mode 100644 lms/djangoapps/certificates/management/commands/gen_cert_report.py diff --git a/lms/djangoapps/certificates/management/commands/find_unicode_certs.py b/lms/djangoapps/certificates/management/commands/find_unicode_certs.py deleted file mode 100644 index 49497622c6..0000000000 --- a/lms/djangoapps/certificates/management/commands/find_unicode_certs.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.core.management.base import BaseCommand -from certificates.models import certificate_status_for_student -from certificates.queue import XQueueCertInterface -from django.contrib.auth.models import User -from student.models import UserProfile - - -class Command(BaseCommand): - - help = """ - Looks for names that have unicode characters - and queues them up for a certificate request - """ - - def handle(self, *args, **options): - - # TODO this is only temporary for CS169 certs - - course_id = 'BerkeleyX/CS169.1x/2012_Fall' - - enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id).prefetch_related( - "groups").order_by('username') - xq = XQueueCertInterface() - print "Looking for unusual names.." - for student in enrolled_students: - if certificate_status_for_student( - student, course_id)['status'] == 'unavailable': - continue - name = UserProfile.objects.get(user=student).name - for c in name: - if ord(c) >= 0x200: - ret = xq.add_cert(student, course_id) - if ret == 'generating': - print 'generating for {0}'.format(student) - break diff --git a/lms/djangoapps/certificates/management/commands/gen_cert_report.py b/lms/djangoapps/certificates/management/commands/gen_cert_report.py new file mode 100644 index 0000000000..6c175dcc4c --- /dev/null +++ b/lms/djangoapps/certificates/management/commands/gen_cert_report.py @@ -0,0 +1,95 @@ +from django.core.management.base import BaseCommand +from certificates.models import certificate_status_for_student +from django.contrib.auth.models import User +from optparse import make_option +from django.conf import settings +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.django import modulestore +from collections import Counter + + +class Command(BaseCommand): + + help = """ + + Generate a certificate status report for all courses that have ended. + This command does not do anything other than report the current + certificate status. + + unavailable - A student is not eligible for a certificate. + generating - A request has been made to generate a certificate, + but it has not been generated yet. + regenerating - A request has been made to regenerate a certificate, + but it has not been generated yet. + deleting - A request has been made to delete a certificate. + + deleted - The certificate has been deleted. + downloadable - The certificate is available for download. + notpassing - The student was graded but is not passing + + """ + + option_list = BaseCommand.option_list + ( + make_option('-c', '--course', + metavar='COURSE_ID', + dest='course', + default=None, + help='Only generate for COURSE_ID'), + ) + + def handle(self, *args, **options): + + # Find all courses that have ended + + if options['course']: + ended_courses = [options['course']] + else: + ended_courses = [] + for course_id in [course # all courses in COURSE_LISTINGS + for sub in settings.COURSE_LISTINGS + for course in settings.COURSE_LISTINGS[sub]]: + + course_loc = CourseDescriptor.id_to_location(course_id) + course = modulestore().get_instance(course_id, course_loc) + if course.has_ended(): + ended_courses.append(course_id) + + total_enrolled = {} + cert_statuses = {} + + for course_id in ended_courses: + + # find students who are enrolled + print "Looking up certificate states for {0}".format(course_id) + enrolled_students = User.objects.filter( + courseenrollment__course_id=course_id).prefetch_related( + "groups").order_by('username') + total_enrolled[course_id] = enrolled_students.count() + + # tally up certificate statuses for every student + # enrolled in the course + cert_statuses[course_id] = Counter( + [certificate_status_for_student( + student, course_id)['status'] + for student in enrolled_students]) + + # all states we have seen far all courses + status_headings = set( + [status for course in cert_statuses + for status in cert_statuses[course]]) + + # print the heading for the report + print "{0:>20}{1:>10}".format("course ID", "enrolled"), + print ' '.join(["{:>12}".format(heading) + for heading in status_headings]) + + # print the report + for course_id in total_enrolled: + print "{0:>20}{1:>10}".format( + course_id[0:18], total_enrolled[course_id]), + for heading in status_headings: + if heading in cert_statuses[course_id]: + print "{:>12}".format(cert_statuses[course_id][heading]), + else: + print " " * 12, + print diff --git a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py index 82e86c2097..080918c0cc 100644 --- a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py +++ b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py @@ -2,29 +2,92 @@ from django.core.management.base import BaseCommand from certificates.models import certificate_status_for_student from certificates.queue import XQueueCertInterface from django.contrib.auth.models import User +from optparse import make_option +from django.conf import settings +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.django import modulestore +from certificates.models import CertificateStatuses +import datetime class Command(BaseCommand): help = """ - Find all students that have need certificates - and put certificate requests on the queue + Find all students that need certificates + for courses that have finished and + put their cert requests on the queue - This is only for BerkeleyX/CS169.1x/2012_Fall + Use the --noop option to test without actually + putting certificates on the queue to be generated. """ + option_list = BaseCommand.option_list + ( + make_option('-n', '--noop', + action='store_true', + dest='noop', + default=False, + help="Don't add certificate requests to the queue"), + make_option('-c', '--course', + metavar='COURSE_ID', + dest='course', + default=False, + help='Grade and generate certificates for a specific course'), + + ) + def handle(self, *args, **options): - # TODO This is only temporary for CS169 certs + # Will only generate a certificate if the current + # status is in this state - course_id = 'BerkeleyX/CS169.1x/2012_Fall' - enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id).prefetch_related( - "groups").order_by('username') - xq = XQueueCertInterface() - for student in enrolled_students: - if certificate_status_for_student( - student, course_id)['status'] == 'unavailable': - ret = xq.add_cert(student, course_id) - if ret == 'generating': - print 'generating for {0}'.format(student) + VALID_STATUSES = [ + CertificateStatuses.unavailable + ] + + # Print update after this many students + + STATUS_INTERVAL = 500 + + if options['course']: + ended_courses = [options['course']] + else: + # Find all courses that have ended + ended_courses = [] + for course_id in [course # all courses in COURSE_LISTINGS + for sub in settings.COURSE_LISTINGS + for course in settings.COURSE_LISTINGS[sub]]: + course_loc = CourseDescriptor.id_to_location(course_id) + course = modulestore().get_instance(course_id, course_loc) + if course.has_ended(): + ended_courses.append(course_id) + + for course_id in ended_courses: + print "Fetching enrolled students for {0}".format(course_id) + enrolled_students = User.objects.filter( + courseenrollment__course_id=course_id).prefetch_related( + "groups").order_by('username') + xq = XQueueCertInterface() + total = enrolled_students.count() + count = 0 + start = datetime.datetime.now() + for student in enrolled_students: + count += 1 + if count % STATUS_INTERVAL == 0: + # Print a status update with an approximation of + # how much time is left based on how long the last + # interval took + diff = datetime.datetime.now() - start + timeleft = diff * (total - count) / STATUS_INTERVAL + hours, remainder = divmod(timeleft.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( + count, total, hours, minutes) + start = datetime.datetime.now() + + if certificate_status_for_student( + student, course_id)['status'] in VALID_STATUSES: + if not options['noop']: + # Add the certificate request to the queue + ret = xq.add_cert(student, course_id) + if ret == 'generating': + print '{0} - {1}'.format(student, ret) From 626e6c7e0849fb4236e9ecb104cf1ef47692bcca Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Thu, 15 Nov 2012 19:05:28 -0500 Subject: [PATCH 003/133] Addresses review comments * List of courses that have ended is now a generator * Using db-aggregation to speed up query for tallies --- .../management/commands/gen_cert_report.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/certificates/management/commands/gen_cert_report.py b/lms/djangoapps/certificates/management/commands/gen_cert_report.py index 6c175dcc4c..8cb6879d34 100644 --- a/lms/djangoapps/certificates/management/commands/gen_cert_report.py +++ b/lms/djangoapps/certificates/management/commands/gen_cert_report.py @@ -1,5 +1,6 @@ from django.core.management.base import BaseCommand from certificates.models import certificate_status_for_student +from certificates.models import CertificateStatuses from django.contrib.auth.models import User from optparse import make_option from django.conf import settings @@ -37,6 +38,15 @@ class Command(BaseCommand): help='Only generate for COURSE_ID'), ) + def _ended_courses(self): + for course_id in [course # all courses in COURSE_LISTINGS + for sub in settings.COURSE_LISTINGS + for course in settings.COURSE_LISTINGS[sub]]: + course_loc = CourseDescriptor.id_to_location(course_id) + course = modulestore().get_instance(course_id, course_loc) + if course.has_ended(): + yield course_id + def handle(self, *args, **options): # Find all courses that have ended @@ -44,15 +54,7 @@ class Command(BaseCommand): if options['course']: ended_courses = [options['course']] else: - ended_courses = [] - for course_id in [course # all courses in COURSE_LISTINGS - for sub in settings.COURSE_LISTINGS - for course in settings.COURSE_LISTINGS[sub]]: - - course_loc = CourseDescriptor.id_to_location(course_id) - course = modulestore().get_instance(course_id, course_loc) - if course.has_ended(): - ended_courses.append(course_id) + ended_courses = self._ended_courses() total_enrolled = {} cert_statuses = {} @@ -66,12 +68,14 @@ class Command(BaseCommand): "groups").order_by('username') total_enrolled[course_id] = enrolled_students.count() + cert_statuses = [attr # all possible certificate statuses + for attr in dir(CertificateStatuses()) + if not callable(attr) and not attr.startswith("__")] + # tally up certificate statuses for every student # enrolled in the course - cert_statuses[course_id] = Counter( - [certificate_status_for_student( - student, course_id)['status'] - for student in enrolled_students]) + cert_statuses = {status:GeneratedCertificate.objects.filter(course_id__exact=course_id, status=status).count() for status in cert_statuses} + # all states we have seen far all courses status_headings = set( From efaf39ea7657bddf7f0c54a8dcdc0d1eed5331be Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Thu, 15 Nov 2012 19:42:47 -0500 Subject: [PATCH 004/133] cleanup --- .../management/commands/gen_cert_report.py | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lms/djangoapps/certificates/management/commands/gen_cert_report.py b/lms/djangoapps/certificates/management/commands/gen_cert_report.py index 8cb6879d34..4fda0078e1 100644 --- a/lms/djangoapps/certificates/management/commands/gen_cert_report.py +++ b/lms/djangoapps/certificates/management/commands/gen_cert_report.py @@ -1,12 +1,11 @@ from django.core.management.base import BaseCommand -from certificates.models import certificate_status_for_student -from certificates.models import CertificateStatuses +from certificates.models import GeneratedCertificate from django.contrib.auth.models import User from optparse import make_option from django.conf import settings from xmodule.course_module import CourseDescriptor from xmodule.modulestore.django import modulestore -from collections import Counter +from django.db.models import Count class Command(BaseCommand): @@ -56,8 +55,7 @@ class Command(BaseCommand): else: ended_courses = self._ended_courses() - total_enrolled = {} - cert_statuses = {} + cert_data = {} for course_id in ended_courses: @@ -66,34 +64,35 @@ class Command(BaseCommand): enrolled_students = User.objects.filter( courseenrollment__course_id=course_id).prefetch_related( "groups").order_by('username') - total_enrolled[course_id] = enrolled_students.count() - - cert_statuses = [attr # all possible certificate statuses - for attr in dir(CertificateStatuses()) - if not callable(attr) and not attr.startswith("__")] - - # tally up certificate statuses for every student - # enrolled in the course - cert_statuses = {status:GeneratedCertificate.objects.filter(course_id__exact=course_id, status=status).count() for status in cert_statuses} + unavailable_count = enrolled_students.count() - \ + GeneratedCertificate.objects.filter( + course_id__exact=course_id).count() + cert_data[course_id] = {'enrolled': enrolled_students.count()} + cert_data[course_id].update({'unavailable': unavailable_count}) + tallies = GeneratedCertificate.objects.values( + 'status').annotate(dcount=Count('status')) + cert_data[course_id].update( + {status['status']: status['dcount'] + for status in tallies}) + print cert_data # all states we have seen far all courses status_headings = set( - [status for course in cert_statuses - for status in cert_statuses[course]]) + [status for course in cert_data + for status in cert_data[course]]) # print the heading for the report - print "{0:>20}{1:>10}".format("course ID", "enrolled"), + print "{:>20}".format("course ID"), print ' '.join(["{:>12}".format(heading) for heading in status_headings]) # print the report - for course_id in total_enrolled: - print "{0:>20}{1:>10}".format( - course_id[0:18], total_enrolled[course_id]), + for course_id in cert_data: + print "{0:>20}".format(course_id[0:18]), for heading in status_headings: - if heading in cert_statuses[course_id]: - print "{:>12}".format(cert_statuses[course_id][heading]), + if heading in cert_data[course_id]: + print "{:>12}".format(cert_data[course_id][heading]), else: print " " * 12, print From 9a12435f6b464071e7f4d915df0c33714ba2ef6b Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Thu, 15 Nov 2012 19:47:33 -0500 Subject: [PATCH 005/133] Removing print --- .../certificates/management/commands/gen_cert_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/certificates/management/commands/gen_cert_report.py b/lms/djangoapps/certificates/management/commands/gen_cert_report.py index 4fda0078e1..21e1775665 100644 --- a/lms/djangoapps/certificates/management/commands/gen_cert_report.py +++ b/lms/djangoapps/certificates/management/commands/gen_cert_report.py @@ -75,7 +75,6 @@ class Command(BaseCommand): cert_data[course_id].update( {status['status']: status['dcount'] for status in tallies}) - print cert_data # all states we have seen far all courses status_headings = set( From c9f7558d6d30031327c137fdd46f7a14c37f5f01 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 16 Nov 2012 18:34:26 -0500 Subject: [PATCH 006/133] first pass at add/remove moderators --- lms/djangoapps/instructor/views.py | 101 +++++++++++++++--- .../courseware/instructor_dashboard.html | 29 +++++ 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0b6392a7fc..0b755145b3 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -1,42 +1,43 @@ # ======== Instructor views ============================================================================= import csv -import itertools -import json +#import itertools +#import json import logging import os import urllib import track.views -from functools import partial +#from functools import partial from collections import defaultdict from django.conf import settings -from django.core.context_processors import csrf -from django.core.urlresolvers import reverse +#from django.core.context_processors import csrf +#from django.core.urlresolvers import reverse from django.contrib.auth.models import User, Group -from django.contrib.auth.decorators import login_required -from django.http import Http404, HttpResponse -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response, render_to_string +#from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +# from django.shortcuts import redirect +from mitxmako.shortcuts import render_to_response #, render_to_string #from django.views.decorators.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access, get_access_group_name -from courseware.courses import (get_course_with_access, get_courses_by_university) +from courseware.courses import get_course_with_access #, get_courses_by_university) from psychometrics import psychoanalyze -from student.models import UserProfile +#from student.models import UserProfile -from student.models import UserTestGroup, CourseEnrollment -from util.cache import cache, cache_if_anonymous +from student.models import CourseEnrollment +# from util.cache import cache, cache_if_anonymous from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location +from django_comment_client.models import Role log = logging.getLogger("mitx.courseware") @@ -203,6 +204,55 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff %s' % user, {}, page='idashboard') + #---------------------------------------- + # forum administration + + + elif action == 'List course forum administrators': + pass + + elif action == 'Remove forum admin': + uname = request.POST['forumadmin'] + msg += _update_forum_role_membership(uname, course_id, 'Administrator', 'remove') + track.views.server_track(request, '%s %s as %s for %s' % ('remove', uname, 'Administrator', course_id), + {}, page='idashboard') + + elif action == 'Add forum admin': + uname = request.POST['forumadmin'] + msg += _update_forum_role_membership(uname, course_id, 'Administrator', 'add') + track.views.server_track(request, '%s %s as %s for %s' % ('add', uname, 'Administrator', course_id), + {}, page='idashboard') + + elif action == 'List course forum moderators': + pass + + elif action == 'Remove forum moderator': + uname = request.POST['forummoderator'] + msg += _update_forum_role_membership(uname, course_id, 'Moderator', 'remove') + track.views.server_track(request, '%s %s as %s for %s' % ('remove', uname, 'Moderator', course_id), + {}, page='idashboard') + + elif action == 'Add forum moderator': + uname = request.POST['forummoderator'] + msg += _update_forum_role_membership(uname, course_id, 'Moderator', 'add') + track.views.server_track(request, '%s %s as %s for %s' % ('add', uname, 'Moderator', course_id), + {}, page='idashboard') + + elif action == 'List course forum community TAs': + pass + + elif action == 'Remove forum community TA': + uname = request.POST['forumcommunityta'] + msg += _update_forum_role_membership(uname, course_id, 'Community TA', 'remove') + track.views.server_track(request, '%s %s as %s for %s' % ('remove', uname, 'Community TA', course_id), + {}, page='idashboard') + + elif action == 'Add forum community TA': + uname = request.POST['forumcommunityta'] + msg += _update_forum_role_membership(uname, course_id, 'Community TA', 'add') + track.views.server_track(request, '%s %s as %s for %s' % ('add', uname, 'Community TA', course_id), + {}, page='idashboard') + #---------------------------------------- # psychometrics @@ -215,6 +265,8 @@ def instructor_dashboard(request, course_id): if idash_mode=='Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) + + #---------------------------------------- # context for rendering context = {'course': course, @@ -232,6 +284,29 @@ def instructor_dashboard(request, course_id): return render_to_response('courseware/instructor_dashboard.html', context) +def _update_forum_role_membership(uname, course_id, rolename, add_or_remove): + ''' + + + returns message status to append to displayed message + ''' + msg = '' + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + return 'Error: unknown username "%s"' % uname + + if user is not None: + role = Role.objects.get(name=rolename, course_id=course_id) + log.debug('rolename=%s' % rolename) + if (add_or_remove == 'remove'): + user.roles.remove(role) + msg += 'Removed %s from %s forum role = %s' % (user, rolename) + else: + user.roles.add(role) + msg += 'Added %s to %s forum role = %s' % (user, rolename) + return msg + def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False): ''' diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index e822f05f92..4a55e4bf14 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -57,6 +57,7 @@ function goto( mode) Psychometrics | %endif Admin ] + Forum Admin ]
${djangopid}
@@ -134,6 +135,34 @@ function goto( mode) %endif %endif +##----------------------------------------------------------------------------- +%if modeflag.get('Forum Admin'): + %if instructor_access: +
+

+ +

+ + +


+ %endif + + %if instructor_access or forum_admin_access: +

+ +

+ + +


+

+ +

+ + +


+ %endif +%endif + ##----------------------------------------------------------------------------- From 5bc0b4864a558363945c0cb701024b4898c3b9fb Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 15:24:23 -0500 Subject: [PATCH 007/133] moving cert link and survey logic into views, adding tests. In-progress. --- common/djangoapps/student/tests.py | 41 +++++++++++----- common/djangoapps/student/views.py | 77 ++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests.py index cde95153fd..8a46b2e458 100644 --- a/common/djangoapps/student/tests.py +++ b/common/djangoapps/student/tests.py @@ -6,11 +6,14 @@ Replace this with more appropriate tests for your application. """ import logging from datetime import datetime +from hashlib import sha1 from django.test import TestCase +from mock import patch, Mock from nose.plugins.skip import SkipTest from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY +import .views COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' @@ -55,7 +58,7 @@ class ReplicationTest(TestCase): # This hasattr lameness is here because we don't want this test to be # triggered when we're being run by CMS tests (Askbot doesn't exist # there, so the test will fail). - # + # # seen_response_count isn't a field we care about, so it shouldn't have # been copied over. if hasattr(portal_user, 'seen_response_count'): @@ -74,7 +77,7 @@ class ReplicationTest(TestCase): # During this entire time, the user data should never have made it over # to COURSE_2 - self.assertRaises(User.DoesNotExist, + self.assertRaises(User.DoesNotExist, User.objects.using(COURSE_2).get, id=portal_user.id) @@ -108,19 +111,19 @@ class ReplicationTest(TestCase): # Grab all the copies we expect course_user = User.objects.using(COURSE_1).get(id=portal_user.id) self.assertEquals(portal_user, course_user) - self.assertRaises(User.DoesNotExist, + self.assertRaises(User.DoesNotExist, User.objects.using(COURSE_2).get, id=portal_user.id) course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id) self.assertEquals(portal_enrollment, course_enrollment) - self.assertRaises(CourseEnrollment.DoesNotExist, + self.assertRaises(CourseEnrollment.DoesNotExist, CourseEnrollment.objects.using(COURSE_2).get, id=portal_enrollment.id) course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id) self.assertEquals(portal_user_profile, course_user_profile) - self.assertRaises(UserProfile.DoesNotExist, + self.assertRaises(UserProfile.DoesNotExist, UserProfile.objects.using(COURSE_2).get, id=portal_user_profile.id) @@ -174,30 +177,44 @@ class ReplicationTest(TestCase): portal_user.save() portal_user_profile.gender = 'm' portal_user_profile.save() - - # Grab all the copies we expect, and make sure it doesn't end up in + + # Grab all the copies we expect, and make sure it doesn't end up in # places we don't expect. course_user = User.objects.using(COURSE_1).get(id=portal_user.id) self.assertEquals(portal_user, course_user) - self.assertRaises(User.DoesNotExist, + self.assertRaises(User.DoesNotExist, User.objects.using(COURSE_2).get, id=portal_user.id) course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id) self.assertEquals(portal_enrollment, course_enrollment) - self.assertRaises(CourseEnrollment.DoesNotExist, + self.assertRaises(CourseEnrollment.DoesNotExist, CourseEnrollment.objects.using(COURSE_2).get, id=portal_enrollment.id) course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id) self.assertEquals(portal_user_profile, course_user_profile) - self.assertRaises(UserProfile.DoesNotExist, + self.assertRaises(UserProfile.DoesNotExist, UserProfile.objects.using(COURSE_2).get, id=portal_user_profile.id) +class CourseEndingTest(TestCase): + """Test things related to course endings: certificates, surveys, etc""" + def test_process_survey_link(self): + username = "fred" + id = sha1(username) + link1 = "http://www.mysurvey.com" + self.assertEqual(process_survey_link(link1), link1) + link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" + link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id) + self.assertEqual(views.process_survey_link(link2), link2_expected) + def test_cert_info(self): + user = Mock(username="fred") + survey_url = "http://a_survey.com" + course = Mock(end_of_course_survey_url=survey_url) + cert_status = None - - + self.assertEqual(views._cert_info(user, course, None), {'status': 'processing'}) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index e7562f83d0..2ebb98da7a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -39,6 +39,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from datetime import date from collections import namedtuple +from hashlib import sha1 + from courseware.courses import get_courses_by_university from courseware.access import has_access @@ -107,9 +109,9 @@ def get_date_for_press(publish_date): # strip off extra months, and just use the first: date = re.sub(multimonth_pattern, ", ", publish_date) if re.search(day_pattern, date): - date = datetime.datetime.strptime(date, "%B %d, %Y") - else: - date = datetime.datetime.strptime(date, "%B, %Y") + date = datetime.datetime.strptime(date, "%B %d, %Y") + else: + date = datetime.datetime.strptime(date, "%B, %Y") return date def press(request): @@ -127,6 +129,73 @@ def press(request): return render_to_response('static_templates/press.html', {'articles': articles}) +def process_survey_link(survey_link, user): + """ + If {UNIQUE_ID} appears in the link, replace it with a unique id for the user. + Currently, this is sha1(user.username). Otherwise, return survey_link. + """ + to_replace = '{UNIQUE_ID}' + if to_replace in survey_link: + unique_id = sha1(user.username) + return survey_link.replace(to_replace, unique_id) + + return survey_link + + +def cert_info(user, course): + """ + Get the certificate info needed to render the dashboard section for the given + student and course. Returns a dictionary with keys: + + 'status': one of 'generating', 'ready', 'notpassing', 'processing' + 'show_download_url': bool + 'download_url': url, only present if show_download_url is True + 'show_survey_button': bool + 'survey_url': url, only if show_survey_button is True + 'grade': if status is not 'processing' + """ + if not course.has_ended(): + return {} + + return _cert_info(user, course, certificate_status_for_student(user, course.id)) + +def _cert_info(user, course, cert_status): + """ + Implements the logic for cert_info -- split out for testing. + """ + default_status = 'processing' + if cert_status is None: + return {'status': default_status} + + # simplify the status for the template using this lookup table + template_state = { + CertificateStatuses.generating: 'generating', + CertificateStatuses.regenerating: 'generating', + CertificateStatuses.downloadable: 'ready', + CertificateStatuses.notpassing: 'notpassing', + } + + status = template_state.get(cert_status['status'], default_status) + + d = {'status': status, + 'show_download_url': status in ('generating', 'ready'),} + + if (status in ('generating', 'ready', 'not-available') and + course.end_of_course_survey_url is not None): + d.update({ + 'show_survey_button': True, + 'survey_url': process_survey_link(course.end_of_course_survey_url, user)}) + else: + d['show_survey_button'] = False + + if template_state == 'ready': + d['download_url'] = cert_status['download_url'] + + if template_state in 'generating', 'ready', 'notpassing': + d['grade'] = cert_status['grade'] + + return d + @login_required @ensure_csrf_cookie def dashboard(request): @@ -163,7 +232,7 @@ def dashboard(request): # TODO: workaround to not have to zip courses and certificates in the template # since before there is a migration to certificates if settings.MITX_FEATURES.get('CERTIFICATES_ENABLED'): - cert_statuses = { course.id: certificate_status_for_student(request.user, course.id) for course in courses} + cert_statuses = { course.id: cert_info(request.user, course) for course in courses} else: cert_statuses = {} From 3a44f043d29060fb7fe997dbec6f6e1dce727394 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 15:58:49 -0500 Subject: [PATCH 008/133] Add tests, make them pass. --- common/djangoapps/student/tests.py | 80 +++++++++++++++++++++++++-- common/djangoapps/student/views.py | 21 ++++--- lms/djangoapps/certificates/models.py | 6 +- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests.py index 8a46b2e458..16eec86379 100644 --- a/common/djangoapps/student/tests.py +++ b/common/djangoapps/student/tests.py @@ -13,7 +13,7 @@ from mock import patch, Mock from nose.plugins.skip import SkipTest from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY -import .views +from .views import process_survey_link, _cert_info, unique_id_for_user COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' @@ -204,17 +204,85 @@ class CourseEndingTest(TestCase): def test_process_survey_link(self): username = "fred" - id = sha1(username) + user = Mock(username=username) + id = unique_id_for_user(user) link1 = "http://www.mysurvey.com" - self.assertEqual(process_survey_link(link1), link1) + self.assertEqual(process_survey_link(link1, user), link1) + link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id) - self.assertEqual(views.process_survey_link(link2), link2_expected) + self.assertEqual(process_survey_link(link2, user), link2_expected) def test_cert_info(self): user = Mock(username="fred") survey_url = "http://a_survey.com" course = Mock(end_of_course_survey_url=survey_url) - cert_status = None - self.assertEqual(views._cert_info(user, course, None), {'status': 'processing'}) + self.assertEqual(_cert_info(user, course, None), + {'status': 'processing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False,}) + + cert_status = {'status': 'unavailable'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'processing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False}) + + cert_status = {'status': 'generating', 'grade': '67'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'generating', + 'show_disabled_download_button': True, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + cert_status = {'status': 'regenerating', 'grade': '67'} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'generating', + 'show_disabled_download_button': True, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + download_url = 'http://s3.edx/cert' + cert_status = {'status': 'downloadable', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'ready', + 'show_disabled_download_button': False, + 'show_download_url': True, + 'download_url': download_url, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + cert_status = {'status': 'notpassing', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course, cert_status), + {'status': 'notpassing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': True, + 'survey_url': survey_url, + 'grade': '67' + }) + + # Test a course that doesn't have a survey specified + course2 = Mock(end_of_course_survey_url=None) + cert_status = {'status': 'notpassing', 'grade': '67', + 'download_url': download_url} + self.assertEqual(_cert_info(user, course2, cert_status), + {'status': 'notpassing', + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False, + 'grade': '67' + }) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 2ebb98da7a..250eddff57 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -129,6 +129,9 @@ def press(request): return render_to_response('static_templates/press.html', {'articles': articles}) +def unique_id_for_user(user): + return sha1(user.username).hexdigest() + def process_survey_link(survey_link, user): """ If {UNIQUE_ID} appears in the link, replace it with a unique id for the user. @@ -136,8 +139,7 @@ def process_survey_link(survey_link, user): """ to_replace = '{UNIQUE_ID}' if to_replace in survey_link: - unique_id = sha1(user.username) - return survey_link.replace(to_replace, unique_id) + return survey_link.replace(to_replace, unique_id_for_user(user)) return survey_link @@ -150,6 +152,7 @@ def cert_info(user, course): 'status': one of 'generating', 'ready', 'notpassing', 'processing' 'show_download_url': bool 'download_url': url, only present if show_download_url is True + 'show_disabled_download_button': bool -- true if state is 'generating' 'show_survey_button': bool 'survey_url': url, only if show_survey_button is True 'grade': if status is not 'processing' @@ -165,7 +168,10 @@ def _cert_info(user, course, cert_status): """ default_status = 'processing' if cert_status is None: - return {'status': default_status} + return {'status': default_status, + 'show_disabled_download_button': False, + 'show_download_url': False, + 'show_survey_button': False} # simplify the status for the template using this lookup table template_state = { @@ -178,9 +184,10 @@ def _cert_info(user, course, cert_status): status = template_state.get(cert_status['status'], default_status) d = {'status': status, - 'show_download_url': status in ('generating', 'ready'),} + 'show_download_url': status == 'ready', + 'show_disabled_download_button': status == 'generating',} - if (status in ('generating', 'ready', 'not-available') and + if (status in ('generating', 'ready', 'notpassing') and course.end_of_course_survey_url is not None): d.update({ 'show_survey_button': True, @@ -188,10 +195,10 @@ def _cert_info(user, course, cert_status): else: d['show_survey_button'] = False - if template_state == 'ready': + if status == 'ready': d['download_url'] = cert_status['download_url'] - if template_state in 'generating', 'ready', 'notpassing': + if status in ('generating', 'ready', 'notpassing'): d['grade'] = cert_status['grade'] return d diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 2d6f384443..b9bd55b9af 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -75,7 +75,9 @@ def certificate_status_for_student(student, course_id): This returns a dictionary with a key for status, and other information. The status is one of the following: - unavailable - A student is not eligible for a certificate. + unavailable - No entry for this student--if they are actually in + the course, they probably have not been graded for + certificate generation yet. generating - A request has been made to generate a certificate, but it has not been generated yet. regenerating - A request has been made to regenerate a certificate, @@ -90,7 +92,7 @@ def certificate_status_for_student(student, course_id): "download_url". If the student has been graded, the dictionary also contains their - grade for the course. + grade for the course with the key "grade". ''' try: From 03dd7b375a6a824ca331822d517d9ad1969adc8e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 15:59:12 -0500 Subject: [PATCH 009/133] get rid of submodule init--not needed post-askbot --- rakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/rakefile b/rakefile index f06b9c3633..c95d822531 100644 --- a/rakefile +++ b/rakefile @@ -54,7 +54,6 @@ default_options = { task :predjango do sh("find . -type f -name *.pyc -delete") sh('pip install -q --upgrade -r local-requirements.txt') - sh('git submodule update --init') end task :clean_test_files do From 003dc7ef517901cc701307f6d761b7014bf4f702 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 16:19:52 -0500 Subject: [PATCH 010/133] Get rid of check for non-existent CERTIFICATES_ENABLED flag... --- common/djangoapps/student/views.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 250eddff57..b354b8c20a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -236,12 +236,7 @@ def dashboard(request): show_courseware_links_for = frozenset(course.id for course in courses if has_access(request.user, course, 'load')) - # TODO: workaround to not have to zip courses and certificates in the template - # since before there is a migration to certificates - if settings.MITX_FEATURES.get('CERTIFICATES_ENABLED'): - cert_statuses = { course.id: cert_info(request.user, course) for course in courses} - else: - cert_statuses = {} + cert_statuses = { course.id: cert_info(request.user, course) for course in courses} context = {'courses': courses, 'message': message, From 61ddec46dd58b2991e19718a23d8aa175ccaa97b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 16:20:17 -0500 Subject: [PATCH 011/133] Use params from view in template. --- lms/templates/dashboard.html | 55 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 2069685a6c..423f53aa35 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -159,54 +159,43 @@ %> % if course.has_ended() and cert_status: <% - passing_grade = False - cert_button = False - survey_button = False - if cert_status['status'] in [CertificateStatuses.generating, CertificateStatuses.regenerating]: + if cert_status['status'] == 'generating': status_css_class = 'course-status-certrendering' - cert_button = True - survey_button = True - passing_grade = True - elif cert_status['status'] == CertificateStatuses.downloadable: + elif cert_status['status'] == 'ready': status_css_class = 'course-status-certavailable' - cert_button = True - survey_button = True - passing_grade = True - elif cert_status['status'] == CertificateStatuses.notpassing: + elif cert_status['status'] == 'notpassing': status_css_class = 'course-status-certnotavailable' - survey_button = True else: - # This is primarily the 'unavailable' state, but also 'error', 'deleted', etc. status_css_class = 'course-status-processing' - - if survey_button and not course.end_of_course_survey_url: - survey_button = False %>
- % if cert_status['status'] == CertificateStatuses.unavailable: -

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

- % elif passing_grade: -

You have received a grade of - ${cert_status['grade']} - in this course.

- % elif cert_status['status'] == CertificateStatuses.notpassing: -

You did not complete the necessary requirements for completion of this course. -

+ % 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'): +

You have received a grade of + ${cert_status['grade']} + in this course.

+ % elif cert_status['status'] == 'notpassing': +

You did not complete the necessary requirements for + completion of this course.

% endif - % if cert_button or survey_button: + + % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: From 71b585bb614d80e79c8866b46145c6bdf6e95f20 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 17 Nov 2012 16:33:00 -0500 Subject: [PATCH 012/133] move unique_id_for_user into student/models.py --- common/djangoapps/student/models.py | 47 +++++++++++++++++++---------- common/djangoapps/student/tests.py | 6 ++-- common/djangoapps/student/views.py | 6 +--- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 0eded21df1..4c91682ca6 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -36,10 +36,12 @@ file and check it in at the same time as your model changes. To do that, 3. Add the migration file created in mitx/common/djangoapps/student/migrations/ """ from datetime import datetime +from hashlib import sha1 import json import logging import uuid + from django.conf import settings from django.contrib.auth.models import User from django.db import models @@ -125,9 +127,9 @@ class UserProfile(models.Model): self.meta = json.dumps(js) class TestCenterUser(models.Model): - """This is our representation of the User for in-person testing, and + """This is our representation of the User 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. * While we have a lot of this demographic data in UserProfile, it's much @@ -135,9 +137,9 @@ class TestCenterUser(models.Model): UserProfile, but we'll need to have a step where people who are signing up re-enter their demographic data into the fields we specify. * Users are only created here if they register to take an exam in person. - + The field names and lengths are modeled on the conventions and constraints - of Pearson's data import system, including oddities such as suffix having + of Pearson's data import system, including oddities such as suffix having a limit of 255 while last_name only gets 50. """ # Our own record keeping... @@ -148,21 +150,21 @@ class TestCenterUser(models.Model): # 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. user_updated_at = models.DateTimeField(db_index=True) - + # Unique ID given to us for this User by the Testing Center. It's null when # 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. client_candidate_id = models.CharField(max_length=50, db_index=True) - + # Name first_name = models.CharField(max_length=30, db_index=True) last_name = models.CharField(max_length=50, db_index=True) middle_name = models.CharField(max_length=30, blank=True) suffix = models.CharField(max_length=255, blank=True) salutation = models.CharField(max_length=50, blank=True) - + # Address address_1 = models.CharField(max_length=40) address_2 = models.CharField(max_length=40, blank=True) @@ -175,7 +177,7 @@ class TestCenterUser(models.Model): postal_code = models.CharField(max_length=16, blank=True, db_index=True) # country is a ISO 3166-1 alpha-3 country code (e.g. "USA", "CAN", "MNG") country = models.CharField(max_length=3, db_index=True) - + # Phone phone = models.CharField(max_length=35) extension = models.CharField(max_length=8, blank=True, db_index=True) @@ -183,14 +185,27 @@ class TestCenterUser(models.Model): fax = models.CharField(max_length=35, blank=True) # fax_country_code required *if* fax is present. fax_country_code = models.CharField(max_length=3, blank=True) - + # Company company_name = models.CharField(max_length=50, blank=True) - + @property def email(self): return self.user.email +def unique_id_for_user(user): + """ + Return a unique id for a user, suitable for inserting into + e.g. personalized survey links. + + Currently happens to be implemented as a sha1 hash of the username + (and thus assumes that usernames don't change). + """ + return sha1(user.username).hexdigest() + + + + ## TODO: Should be renamed to generic UserGroup, and possibly # Given an optional field for type of group class UserTestGroup(models.Model): @@ -363,10 +378,10 @@ def replicate_user_save(sender, **kwargs): # @receiver(post_save, sender=CourseEnrollment) def replicate_enrollment_save(sender, **kwargs): - """This is called when a Student enrolls in a course. It has to do the + """This is called when a Student enrolls in a course. It has to do the following: - 1. Make sure the User is copied into the Course DB. It may already exist + 1. Make sure the User is copied into the Course DB. It may already exist (someone deleting and re-adding a course). This has to happen first or the foreign key constraint breaks. 2. Replicate the CourseEnrollment. @@ -410,9 +425,9 @@ USER_FIELDS_TO_COPY = ["id", "username", "first_name", "last_name", "email", def replicate_user(portal_user, course_db_name): """Replicate a User to the correct Course DB. This is more complicated than - it should be because Askbot extends the auth_user table and adds its own + it should be because Askbot extends the auth_user table and adds its own fields. So we need to only push changes to the standard fields and leave - the rest alone so that Askbot changes at the Course DB level don't get + the rest alone so that Askbot changes at the Course DB level don't get overridden. """ try: @@ -457,7 +472,7 @@ def is_valid_course_id(course_id): """Right now, the only database that's not a course database is 'default'. I had nicer checking in here originally -- it would scan the courses that were in the system and only let you choose that. But it was annoying to run - tests with, since we don't have course data for some for our course test + tests with, since we don't have course data for some for our course test databases. Hence the lazy version. """ return course_id != 'default' diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests.py index 16eec86379..4c7c9e2592 100644 --- a/common/djangoapps/student/tests.py +++ b/common/djangoapps/student/tests.py @@ -12,8 +12,10 @@ from django.test import TestCase from mock import patch, Mock from nose.plugins.skip import SkipTest -from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY -from .views import process_survey_link, _cert_info, unique_id_for_user +from .models import (User, UserProfile, CourseEnrollment, + replicate_user, USER_FIELDS_TO_COPY, + unique_id_for_user) +from .views import process_survey_link, _cert_info COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b354b8c20a..ac3a97bd89 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -28,7 +28,7 @@ from django.core.cache import cache from django_future.csrf import ensure_csrf_cookie, csrf_exempt from student.models import (Registration, UserProfile, PendingNameChange, PendingEmailChange, - CourseEnrollment) + CourseEnrollment, unique_id_for_user) from certificates.models import CertificateStatuses, certificate_status_for_student @@ -39,7 +39,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from datetime import date from collections import namedtuple -from hashlib import sha1 from courseware.courses import get_courses_by_university from courseware.access import has_access @@ -129,9 +128,6 @@ def press(request): return render_to_response('static_templates/press.html', {'articles': articles}) -def unique_id_for_user(user): - return sha1(user.username).hexdigest() - def process_survey_link(survey_link, user): """ If {UNIQUE_ID} appears in the link, replace it with a unique id for the user. From 8b1b63003b901498d0e796fd53e9bde8565997b1 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 19 Nov 2012 00:37:43 -0500 Subject: [PATCH 013/133] add listing forum moderator to instructor dash --- lms/djangoapps/instructor/views.py | 34 +++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0b755145b3..5ae92ceb3b 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -46,6 +46,7 @@ template_imports = {'urllib': urllib} @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) + def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course = get_course_with_access(request.user, course_id, 'staff') @@ -209,7 +210,11 @@ def instructor_dashboard(request, course_id): elif action == 'List course forum administrators': - pass + rolename = 'Administrator' + datatable = {} + msg += _list_course_forum_members(course_id, rolename, datatable) + track.views.server_track(request, 'list-%s' % rolename, {}, page='idashboard') + elif action == 'Remove forum admin': uname = request.POST['forumadmin'] @@ -224,7 +229,10 @@ def instructor_dashboard(request, course_id): {}, page='idashboard') elif action == 'List course forum moderators': - pass + rolename = 'Moderator' + datatable = {} + msg += _list_course_forum_members(course_id, rolename, datatable) + track.views.server_track(request, 'list-%s' % rolename, {}, page='idashboard') elif action == 'Remove forum moderator': uname = request.POST['forummoderator'] @@ -239,7 +247,10 @@ def instructor_dashboard(request, course_id): {}, page='idashboard') elif action == 'List course forum community TAs': - pass + rolename = 'Community TA' + datatable = {} + msg += _list_course_forum_members(course_id, rolename, datatable) + track.views.server_track(request, 'list-%s' % rolename, {}, page='idashboard') elif action == 'Remove forum community TA': uname = request.POST['forumcommunityta'] @@ -284,6 +295,19 @@ def instructor_dashboard(request, course_id): return render_to_response('courseware/instructor_dashboard.html', context) +def _list_course_forum_members(course_id, rolename, datatable): + ''' TODO + ''' + role = Role.objects.get(name=rolename, course_id=course_id) + uset = role.users.all() + msg = 'Role = %s' % rolename + log.debug('role=%s' % rolename) + datatable['header'] = ['Username', 'Full name', 'Roles'] + datatable['data'] = [[x.username, x.profile.name, ', '.join([r.name for r in x.roles.all()])] for x in uset] + datatable['title'] = 'List of Forum %s in course %s' % (rolename, course_id) + return msg + + def _update_forum_role_membership(uname, course_id, rolename, add_or_remove): ''' @@ -301,10 +325,10 @@ def _update_forum_role_membership(uname, course_id, rolename, add_or_remove): log.debug('rolename=%s' % rolename) if (add_or_remove == 'remove'): user.roles.remove(role) - msg += 'Removed %s from %s forum role = %s' % (user, rolename) + msg += 'Removed %s from %s forum role = %s' % (user, course_id, rolename) else: user.roles.add(role) - msg += 'Added %s to %s forum role = %s' % (user, rolename) + msg += 'Added %s to %s forum role = %s' % (user, course_id, rolename) return msg From 6326e4dc7e5d2a0a03245447ff8587914f28df80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 15 Nov 2012 13:28:03 -0500 Subject: [PATCH 014/133] Add stub for Gates foundation announcement [#39636489 #39636491 #39636495 #39636903] --- lms/static/images/press/mass_seal_240x180.png | Bin 0 -> 21604 bytes lms/templates/feed.rss | 9 ++ .../Gates_Foundation_announcement.html | 77 ++++++++++++++++++ lms/urls.py | 2 + 4 files changed, 88 insertions(+) create mode 100644 lms/static/images/press/mass_seal_240x180.png create mode 100644 lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html diff --git a/lms/static/images/press/mass_seal_240x180.png b/lms/static/images/press/mass_seal_240x180.png new file mode 100644 index 0000000000000000000000000000000000000000..6641d4e42df1f47890f009b68b25e728ee1a4b1b GIT binary patch literal 21604 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 EdX Blog 2012-10-14T14:08:12-07:00 + + tag:www.edx.org,2012:Post/7 + 2012-11-12T14:00:00-07:00 + 2012-11-12T14:00:00-07:00 + + TITLE + <img src="${static.url('images/press/mass_seal_240x180.png')}" /> + <p>CONTENT</p> + tag:www.edx.org,2012:Post/6 2012-10-15T14:00:00-07:00 diff --git a/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html b/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html new file mode 100644 index 0000000000..e87fcd8aec --- /dev/null +++ b/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html @@ -0,0 +1,77 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">TITLE +
+ + +
+
+

TITLE

+
+
+

SUBTITLE

+ +

LOCATION – November 12, 2012 — THE_CONTENT

+ +

About edX

+ +

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

+ +

About Harvard University

+ +

Harvard University is devoted to excellence in teaching, learning and research, and to developing leaders in many disciplines who make a difference globally. Harvard Faculty are engaged with teaching and research to push the boundaries of human knowledge. The University has twelve degree-granting Schools in addition to the Radcliffe Institute for Advanced Study.

+ +

Established in 1636, Harvard is the oldest institution of higher education in the United States. The University, which is based in Cambridge and Boston, Massachusetts, has an enrollment of over 20,000 degree candidates, including undergraduate, graduate and professional students. Harvard has more than 360,000 alumni around the world.

+ +

About MIT

+

The Massachusetts Institute of Technology — a coeducational, privately endowed research university founded in 1861 — is dedicated to advancing knowledge and educating students in science, technology and other areas of scholarship that will best serve the nation and the world in the 21st century. The Institute has close to 1,000 faculty and 10,000 undergraduate and graduate students. It is organized into five Schools: Architecture and Urban Planning; Engineering; Humanities, Arts, and Social Sciences; Sloan School of Management; and Science.

+ +

MIT's commitment to innovation has led to a host of scientific breakthroughs and technological advances. Achievements of the Institute's faculty and graduates have included the first chemical synthesis of penicillin and vitamin A, the development of inertial guidance systems, modern technologies for artificial limbs and the magnetic core memory that made possible the development of digital computers. Seventy-eight alumni, faculty, researchers and staff have won Nobel Prizes.

+ +

Current areas of research and education include neuroscience and the study of the brain and mind, bioengineering, cancer, energy, the environment and sustainable development, information sciences and technology, new media, financial technology and entrepreneurship.

+ +

About the University of California, Berkeley

+ +

The University of California, Berkeley is the world's premier public university with a mission to excel in teaching, research and public service. This longstanding mission has led to the university's distinguished record of Nobel-level scholarship, constant innovation, a concern for the betterment of our world, and consistently high rankings of its schools and departments. The campus offers superior, high value education for extraordinarily talented students from all walks of life; operational excellence and a commitment to the competitiveness and prosperity of California and the nation.

+ +

The University of California was chartered in 1868 and its flagship campus in Berkeley, on San Francisco Bay, was envisioned as a “City of Learning.” Today, there are more than 1,500 fulltime and 500 part-time faculty members dispersed among more than 130 academic departments and more than 80 interdisciplinary research units. Twenty-two Nobel Prizes have been garnered by faculty and 28 by UC Berkeley alumni. There are 9 Nobel Laureates, 32 MacArthur Fellows, and 4 Pulitzer Prize winners among the current faculty.

+ +

About The University of Texas System

+ +

Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state's undergraduate degrees and educates nearly three-fourths of the state's health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state. www.utsystem.edu

+ +
+

edX Contact: Dan O’Connell

+

oconnell@edx.org

+

617-480-6585

+
+ + +
+
+
diff --git a/lms/urls.py b/lms/urls.py index 527f3f858e..529396c20e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -101,6 +101,8 @@ urlpatterns = ('', {'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"), url(r'^press/cengage-to-provide-book-content$', 'static_template_view.views.render', {'template': 'press_releases/Cengage_to_provide_book_content.html'}, name="press/cengage-to-provide-book-content"), + url(r'^press/gates-foundation-announcement$', 'static_template_view.views.render', + {'template': 'press_releases/Gates_Foundation_announcement.html'}, name="press/gates-foundation-announcement"), # Should this always update to point to the latest press release? (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), From cabbc8906a76273a712b4e31986cf3437a79fd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Fri, 16 Nov 2012 17:31:39 -0500 Subject: [PATCH 015/133] Add press release text and update news feed [#39636489 #39636491 #39636495 #39636903] --- lms/templates/feed.rss | 4 +- .../Gates_Foundation_announcement.html | 58 ++++++++----------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index 9df3c29633..4bd66db799 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -12,9 +12,9 @@ 2012-11-12T14:00:00-07:00 2012-11-12T14:00:00-07:00 - TITLE + edX and Massachusetts Community Colleges join in Gates-Funded educational initiative <img src="${static.url('images/press/mass_seal_240x180.png')}" /> - <p>CONTENT</p> + <p></p>
tag:www.edx.org,2012:Post/6 diff --git a/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html b/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html index e87fcd8aec..0cdd51e90b 100644 --- a/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html +++ b/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html @@ -3,7 +3,7 @@ <%namespace name='static' file='../../static_content.html'/> -<%block name="title">TITLE +<%block name="title">edX and Massachusetts Community Colleges Join in Gates-Funded Educational Initiative
+ From c2c016c58b19646d90aea65dfe12753f0fad1e82 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 30 Nov 2012 18:00:45 -0500 Subject: [PATCH 102/133] Fix MathJax S3 URL to use the standard HTTPS location --- common/templates/mathjax_include.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html index a5a63e70ed..803f2145a4 100644 --- a/common/templates/mathjax_include.html +++ b/common/templates/mathjax_include.html @@ -33,4 +33,4 @@ - + From 7ffb30d0c995e86de8e947862247bb9abf5c0de7 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 30 Nov 2012 18:22:37 -0500 Subject: [PATCH 103/133] IE errors if you try to use 'return' as a dict key. --- lms/templates/courseware/xqa_interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/courseware/xqa_interface.html b/lms/templates/courseware/xqa_interface.html index 73f7cc6f52..c314cc7fb0 100644 --- a/lms/templates/courseware/xqa_interface.html +++ b/lms/templates/courseware/xqa_interface.html @@ -14,7 +14,7 @@ function sendlog(element_id, edit_link, staff_context){ location: staff_context.location, category : staff_context.category, 'username' : staff_context.user.username, - return : 'query', + 'return' : 'query', format : 'html', email : staff_context.user.email, tag:$('#' + element_id + '_xqa_tag').val(), From 0c1ebd8dfb3a10f0b6321d72c38adca02de28e1a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 30 Nov 2012 19:05:26 -0500 Subject: [PATCH 104/133] add toylab test data and test changes, including fix to jump_to --- common/lib/xmodule/xmodule/html_module.py | 4 +- common/test/data/toy/html/secret/toylab.html | 4 + lms/djangoapps/courseware/tests/tests.py | 101 +++++++++++++++---- lms/djangoapps/courseware/views.py | 17 ++-- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 4f10cc84f1..eea747e332 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -30,7 +30,9 @@ class HtmlModule(XModule): def get_html(self): # cdodge: perform link substitutions for any references to course static content (e.g. images) - return rewrite_links(self.html, self.rewrite_content_links) + output = rewrite_links(self.html, self.rewrite_content_links) + log.info(' HTMLModule converting markup "{0}" to "{1}"'.format(self.html, output)) + return output def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/common/test/data/toy/html/secret/toylab.html b/common/test/data/toy/html/secret/toylab.html index 760482c4a0..b2a4599cc6 100644 --- a/common/test/data/toy/html/secret/toylab.html +++ b/common/test/data/toy/html/secret/toylab.html @@ -1,5 +1,6 @@ Lab 2A: Superposition Experiment +<<<<<<< Updated upstream

Isn't the toy course great?

Let's add some markup that uses non-ascii characters. @@ -7,3 +8,6 @@ For example, we should be able to write words like encyclopædia, or foreig Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π.

+======= +

Isn't the toy course great? — ≤

+>>>>>>> Stashed changes diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 8239eadfd9..defbf426cc 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,21 +1,15 @@ -import copy import json import os -import sys import time from nose import SkipTest -from path import path -from pprint import pprint from urlparse import urlsplit, urlunsplit from django.contrib.auth.models import User, Group -from django.core.handlers.wsgi import WSGIRequest from django.test import TestCase -from django.test.client import Client, RequestFactory +from django.test.client import RequestFactory from django.conf import settings from django.core.urlresolvers import reverse -from mock import patch, Mock from override_settings import override_settings import xmodule.modulestore.django @@ -29,6 +23,7 @@ from student.models import Registration from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml import XMLModuleStore from xmodule.timeparse import stringify_time def parse_json(response): @@ -76,10 +71,21 @@ def xml_store_config(data_dir): } } +def my_xml_store_config(data_dir): + return { + 'default': { + 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', + 'OPTIONS': { + 'data_dir': data_dir, + 'default_class': 'xmodule.hidden_module.HiddenDescriptor', + } + } +} TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +# TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +MY_TEST_DATA_XML_MODULESTORE = my_xml_store_config(TEST_DATA_DIR) REAL_DATA_DIR = settings.GITHUB_REPO_ROOT REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR) @@ -252,34 +258,93 @@ class PageLoader(ActivateLoginTestCase): #print descriptor.__class__, descriptor.location resp = self.client.get(reverse('jump_to', kwargs={'course_id': course_id, - 'location': descriptor.location.url()})) + 'location': descriptor.location.url()}), follow=True) msg = str(resp.status_code) - - if resp.status_code != 302: + if resp.status_code != 200: + msg = "ERROR " + msg # + ": " + str(resp.request['PATH_INFO']) + all_ok = False + num_bad += 1 + elif resp.redirect_chain[0][1] != 302: msg = "ERROR " + msg all_ok = False num_bad += 1 print msg - self.assertTrue(all_ok) # fail fast +# self.assertTrue(all_ok) # fail fast + + print "{0}/{1} good".format(n - num_bad, n) + self.assertTrue(all_ok) + + def check_xml_pages_load(self, course_name, data_dir, modstore): + """Make all locations in course load""" + print "Checking course {0} in {1}".format(course_name, data_dir) + default_class='xmodule.hidden_module.HiddenDescriptor' # 'xmodule.raw_module.RawDescriptor', + load_error_modules=True + module_store = XMLModuleStore( + data_dir, + default_class=default_class, + course_dirs=[course_name], + load_error_modules=load_error_modules, + ) +# for course_id in module_store.modules.keys(): +# for module in module_store.modules[course_id].itervalues(): +# +# if 'data' in module.definition: +# store.update_item(module.location, module.definition['data']) +# if 'children' in module.definition: +# store.update_children(module.location, module.definition['children']) +# # NOTE: It's important to use own_metadata here to avoid writing +# # inherited metadata everywhere. +# store.update_metadata(module.location, dict(module.own_metadata)) + # enroll in the course before trying to access pages + courses = module_store.get_courses() + self.assertEqual(len(courses), 1) + course = courses[0] + self.enroll(course) + course_id = course.id + + n = 0 + num_bad = 0 + all_ok = True + for descriptor in module_store.modules[course_id].itervalues(): + n += 1 + print "Checking ", descriptor.location.url() + #print descriptor.__class__, descriptor.location + resp = self.client.get(reverse('jump_to', + kwargs={'course_id': course_id, + 'location': descriptor.location.url()}), follow=True) + msg = str(resp.status_code) + if resp.status_code != 200: + msg = "ERROR " + msg # + ": " + str(resp.request['PATH_INFO']) + all_ok = False + num_bad += 1 + elif resp.redirect_chain[0][1] != 302: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 + print msg +# self.assertTrue(all_ok) # fail fast print "{0}/{1} good".format(n - num_bad, n) self.assertTrue(all_ok) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +#@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=MY_TEST_DATA_XML_MODULESTORE) class TestCoursesLoadTestCase(PageLoader): '''Check that all pages in test courses load properly''' def setUp(self): ActivateLoginTestCase.setUp(self) xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - +# xmodule.modulestore.django.modulestore().collection.drop() +# store = xmodule.modulestore.django.modulestore() + # is there a way to empty the store? + def test_toy_course_loads(self): - self.check_pages_load('toy', TEST_DATA_DIR, modulestore()) + self.check_xml_pages_load('toy', TEST_DATA_DIR, modulestore()) - def test_full_course_loads(self): - self.check_pages_load('full', TEST_DATA_DIR, modulestore()) +# def test_full_course_loads(self): +# self.check_pages_load('full', TEST_DATA_DIR, modulestore()) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 73d40b05c5..276af80ca9 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -293,7 +293,6 @@ def index(request, course_id, chapter=None, section=None, return result - @ensure_csrf_cookie def jump_to(request, course_id, location): ''' @@ -318,12 +317,18 @@ def jump_to(request, course_id, location): except NoPathToItem: raise Http404("This location is not in any class: {0}".format(location)) + # choose the appropriate view (and provide the necessary args) based on the + # args provided by the redirect. # Rely on index to do all error handling and access control. - return redirect('courseware_position', - course_id=course_id, - chapter=chapter, - section=section, - position=position) + if chapter is None: + return redirect('courseware', course_id=course_id) + elif section is None: + return redirect('courseware_chapter', course_id=course_id, chapter=chapter) + elif position is None: + return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section) + else: + return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position) + @ensure_csrf_cookie def course_info(request, course_id): """ From 3b326048f9087b34cb8b0c74618fba3cee464da4 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sat, 1 Dec 2012 12:16:10 -0500 Subject: [PATCH 105/133] Don't log errors to console if there is no console (like IE8 and below). --- lms/static/coffee/src/courseware.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index 096094ead9..0992043e79 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -18,5 +18,6 @@ class @Courseware histg = new Histogram id, $(this).data('histogram') catch error histg = error - console.log(error) + if console? + console.log(error) return histg From 251f552e64fb5ead819d823b4690e10f47258691 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 3 Dec 2012 14:30:36 -0500 Subject: [PATCH 106/133] Don't try and send exceptions to newrelic with a busted logging interface --- lms/envs/logsettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/logsettings.py b/lms/envs/logsettings.py index 2b001b0517..8bd61a9e67 100644 --- a/lms/envs/logsettings.py +++ b/lms/envs/logsettings.py @@ -40,7 +40,7 @@ def get_logger_config(log_dir, logging_env=logging_env, hostname=hostname) handlers = ['console', 'local'] if debug else ['console', - 'syslogger-remote', 'local', 'newrelic'] + 'syslogger-remote', 'local'] logger_config = { 'version': 1, From 1d44ebb10c842b02371dd92af02db671785ce0b0 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 3 Dec 2012 16:22:25 -0500 Subject: [PATCH 107/133] Add more non-ascii characters to full dataset, and make fixes in xmodule code. --- common/djangoapps/mitxmako/shortcuts.py | 7 ++++- common/djangoapps/mitxmako/template.py | 11 +++++-- common/lib/xmodule/xmodule/html_module.py | 6 ++-- common/test/data/full/custom_tags/book | 2 +- common/test/data/full/custom_tags/discuss | 2 +- common/test/data/full/custom_tags/slides | 2 +- .../data/full/problem/Circuit_Sandbox.xml | 4 +-- .../data/full/problem/choiceresponse_demo.xml | 15 ++++----- .../test/data/full/problem/codeinput_demo.xml | 3 +- .../Administrivia_and_Circuit_Elements.xml | 4 +-- .../test/data/full/vertical/vertical_89.xml | 2 +- common/test/data/full/video/welcome.xml | 2 +- lms/djangoapps/courseware/tests/tests.py | 31 ++++++++++++++++--- 13 files changed, 64 insertions(+), 27 deletions(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index 181d3befd5..6aee39906a 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -42,7 +42,12 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary.update(context) # fetch and render template template = middleware.lookup[namespace].get_template(template_name) - return template.render_unicode(**context_dictionary) +# return template.render_unicode(**context_dictionary) + + output = template.render_unicode(**context_dictionary) +# log.info(' render_to_string of "{0}" as "{1}r"'.format(type(output), output)) + return output +# return template.render(**context_dictionary) def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py index 2d6fc026ca..efeb282d04 100644 --- a/common/djangoapps/mitxmako/template.py +++ b/common/djangoapps/mitxmako/template.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +log = logging.getLogger("mitx." + __name__) + from django.conf import settings from mako.template import Template as MakoTemplate @@ -54,5 +57,9 @@ class Template(MakoTemplate): context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL context_dictionary['django_context'] = context_instance - return super(Template, self).render_unicode(**context_dictionary) - +# return super(Template, self).render_unicode(**context_dictionary) +# return super(Template, self).render(**context_dictionary) + + output = super(Template, self).render(**context_dictionary) + log.info(' render_to_string of "{0}" as "{1}"'.format(type(output), output)) + return output diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index eea747e332..6b73535dac 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -30,8 +30,10 @@ class HtmlModule(XModule): def get_html(self): # cdodge: perform link substitutions for any references to course static content (e.g. images) + input = self.html output = rewrite_links(self.html, self.rewrite_content_links) - log.info(' HTMLModule converting markup "{0}" to "{1}"'.format(self.html, output)) +# log.info(' HTMLModule converting markup from "{0}" as "{1}r"'.format(type(input), input)) +# log.info(' HTMLModule converting markup to "{0}" as "{1}r"'.format(type(output), output)) return output def __init__(self, system, location, definition, descriptor, @@ -166,7 +168,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(self.definition['data'].encode('utf-8')) + file.write(self.definition['data']) # .encode('utf-8')) # write out the relative name relname = path(pathname).basename() diff --git a/common/test/data/full/custom_tags/book b/common/test/data/full/custom_tags/book index ece6f288db..32e0913e6d 100644 --- a/common/test/data/full/custom_tags/book +++ b/common/test/data/full/custom_tags/book @@ -1 +1 @@ -More information given in the text. +More information given in… the text. diff --git a/common/test/data/full/custom_tags/discuss b/common/test/data/full/custom_tags/discuss index ac56590074..7a8a9e985f 100644 --- a/common/test/data/full/custom_tags/discuss +++ b/common/test/data/full/custom_tags/discuss @@ -1 +1 @@ - Discussion: ${tag} \ No newline at end of file + Discussion: ${tag}… \ No newline at end of file diff --git a/common/test/data/full/custom_tags/slides b/common/test/data/full/custom_tags/slides index a93d94947c..967c203711 100644 --- a/common/test/data/full/custom_tags/slides +++ b/common/test/data/full/custom_tags/slides @@ -1 +1 @@ -Lecture Slides Handout [Clean ][Annotated] +Lecture Slides Handout [Clean… ][Annotated…] diff --git a/common/test/data/full/problem/Circuit_Sandbox.xml b/common/test/data/full/problem/Circuit_Sandbox.xml index 89625f447b..1582f3ff0b 100644 --- a/common/test/data/full/problem/Circuit_Sandbox.xml +++ b/common/test/data/full/problem/Circuit_Sandbox.xml @@ -1,6 +1,6 @@ -

Here's a sandbox where you can experiment with all the components +

Here's a sandbox where you can experiment with all the components we'll discuss in 6.002x. If you click on CHECK below, your diagram -will be saved on the server and you can return at some later time. +will be saved on the server and you can return at some later time…

correct = ['correct']
diff --git a/common/test/data/full/problem/choiceresponse_demo.xml b/common/test/data/full/problem/choiceresponse_demo.xml index f7d1fcf16c..7af7939d74 100644 --- a/common/test/data/full/problem/choiceresponse_demo.xml +++ b/common/test/data/full/problem/choiceresponse_demo.xml @@ -1,19 +1,20 @@ -

Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to leftin the plane of your screen. A diagram of this situation is show below.

+

Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to left in the plane of your screen. A diagram of this situation is show below…

a. The magnitude of the force experienced by the electron is proportional the product of which of the following? (Select all that apply.)

-Magnetic field strength -Electric field strength -Electric charge of the electron -Radius of the electron -Mass of the electron -Velocity of the electron + +Magnetic field strength… +Electric field strength… +Electric charge of the electron… +Radius of the electron… +Mass of the electron… +Velocity of the electron… diff --git a/common/test/data/full/problem/codeinput_demo.xml b/common/test/data/full/problem/codeinput_demo.xml index 03d8fd8c31..a6662cb69c 100644 --- a/common/test/data/full/problem/codeinput_demo.xml +++ b/common/test/data/full/problem/codeinput_demo.xml @@ -2,7 +2,8 @@

- Part 1: Function Types + + Part 1: Function Types…

For each of the following functions, specify the type of its output. You can assume each function is called with an appropriate argument, as specified by its docstring.

diff --git a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml index 5c4c65f12d..d0239198af 100644 --- a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml +++ b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml @@ -3,12 +3,12 @@ - S1E4 has been removed. + S1E4 has been removed… diff --git a/common/test/data/full/vertical/vertical_89.xml b/common/test/data/full/vertical/vertical_89.xml index da15a6751a..a4716366fe 100644 --- a/common/test/data/full/vertical/vertical_89.xml +++ b/common/test/data/full/vertical/vertical_89.xml @@ -1,6 +1,6 @@ -

+

Inline content…

-
  • +
  • @@ -81,6 +81,15 @@
  • +
  • + + +
    + WellesleyX +
    +
    +
  • + diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index a8f6268bd5..acd00bafe8 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -21,19 +21,25 @@

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

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

    +
    -

    Why is The University of Texas System joining edX?

    -

    Joining edX not only allows UT faculty to showcase their work on a global stage, but also provides UT students the opportunity to take classes from their choice of UT institutions, as well as MIT, Harvard, UC Berkeley and future “X” Universities.

    -

    The UT System closely examined all the alternatives and determined that edX offered the best fit in terms of alignment of mission, platform and revenue model. The strength and reputation of the partner institutions – MIT, Harvard and UC Berkeley – was also a huge consideration. EdX is committed to both blended and online learning and to a non-profit, open source model. It is also governed by a board of academics with a commitment to excellence in learning.

    +

    Why is Wellesley College joining edX?

    +

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

    +

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

    +

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

    -

    What will The UT System’s direct participation entail?

    -

    The UT System will begin by offering one course on edX from The University of Texas at Austin in Summer 2013, and four courses in Fall 2013, likely at least one of those courses from one of its health institutions. The UT System is also making a $5 million investment in the edX platform. We will explore, experiment and innovate together.

    +

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

    +

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

    -

    Will edX be adding additional X Universities?

    -

    More than 140 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 and the UT System will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities” as soon as possible. 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 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 we look forward to adding more “X Universities” as capacity increases.

    +

    How many WellesleyX courses will be offered initially? When?

    +

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

    +
    +
    +

    Will edX be adding additional X Universities?

    +

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

    +

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

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

    Wellesley College becomes first liberal arts college to join edX

    +
    +
    +

    Wellesley joins edX to advance learning collaborative, broadens course options while bringing a unique small classroom experience to the world of massive open online courses

    + +

    CAMBRIDGE, MA – December 04, 2012 — edX, the online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT) and launched in May, announced today the addition of Wellesley College to its group of educational leaders who are focused on providing a category-leading quality higher education experience to the global online community. Wellesley College is the first liberal arts college to join edX—and the first women’s college to offer massive open online courses (MOOCs). Wellesley College will provide a series of WellesleyX courses to the platform that are unique to the College and broaden the course offerings on edx.org.

    + +

    According to H. Kim Bottomly, President of Wellesley College, WellesleyX provides an opportunity for the College to impact the future of higher education. “Wellesley is ready to contribute our liberal arts perspective to help shape online education, particularly as colleges work to figure out how to bring the small classroom experience to the online learning landscape. We are convinced that Wellesley and its outstanding faculty have the creativity and vision to take on this challenge.”

    + +

    Bottomly added, “This is a grand experiment, and what we learn will benefit Wellesley students as well as students all over the world.

    + +

    Regarded as one of the world’s finest colleges, Wellesley is known for cultivating generations of women leaders; its pedagogical innovation; and its commitment to highly personalized, discussion-based learning. With the launch of WellesleyX, the College will open access to its rigorous courses and distinguished faculty to anyone with an internet connection.

    + +

    “We are excited that Wellesley College has chosen to join with edX,” said Anant Agarwal, President of edX. “Wellesley’s long history of educating women leaders in diplomacy, the arts, science and business provides a unique strength. We look forward to working alongside the Wellesley faculty to extend their reach to hundreds of thousands of women and men around the world.”

    + +

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

    + +

    “Wellesley College is a welcome addition to edX and our efforts to fully realize the potential of online education for students on campus and online,” said Harvard President Drew Faust. “As an institution that has provided an outstanding educational experience to many thousands of women for over 100 years, Wellesley brings to edX both a unique academic perspective and a commitment to excellence in education.”

    + +

    “Wellesley College's decision to join the edX platform is excellent news for edX and for the platform's growing number of users around the world,” said MIT President L. Rafael Reif. “Wellesley brings a distinctive history that will further enrich the efforts we are making to tailor instruction to the different ways by which people learn.”

    + +

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

    + +

    About edX

    + +

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

    + +

    About Wellesley College

    + +

    Since 1875, Wellesley College has been the preeminent liberal arts college for women. Known for its intellectual rigor and its remarkable track record for the cultivation of women leaders in every arena, Wellesley—only 12 miles from Boston—is home to some 2300 undergraduates from every state and 75 countries.

    + +
    +

    Contact: Amanda Keane

    +

    akeane@webershandwick.com

    +

    617-520-7260

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

    Since 1875, Wellesley College has been the preeminent liberal arts college for women. Known for its intellectual rigor and its remarkable track record for the cultivation of women leaders in every arena, Wellesley—only 12 miles from Boston—is home to some 2300 undergraduates from every state and 75 countries.

    + + +${parent.body()} diff --git a/lms/urls.py b/lms/urls.py index 529396c20e..a3c61a2687 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -61,6 +61,7 @@ urlpatterns = ('', url(r'^heartbeat$', include('heartbeat.urls')), url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'UTx'}), + url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'WellesleyX'}), url(r'^university_profile/(?P[^/]+)$', 'courseware.views.university_profile', name="university_profile"), #Semi-static views (these need to be rendered and have the login bar, but don't change) @@ -103,9 +104,12 @@ urlpatterns = ('', {'template': 'press_releases/Cengage_to_provide_book_content.html'}, name="press/cengage-to-provide-book-content"), url(r'^press/gates-foundation-announcement$', 'static_template_view.views.render', {'template': 'press_releases/Gates_Foundation_announcement.html'}, name="press/gates-foundation-announcement"), + url(r'^press/wellesley-college-joins-edx$', 'static_template_view.views.render', + {'template': 'press_releases/Wellesley_College_joins_edX.html'}, name="press/wellesley-college-joins-edx"), + # Should this always update to point to the latest press release? - (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), + (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/wellesley-college-joins-edx'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), From 4d6162b1df365e7893a80a1b06aa415779ff2f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 4 Dec 2012 09:58:50 -0500 Subject: [PATCH 111/133] Fix incorrect Facebook URL for Wellesley news announcement --- .../press_releases/Wellesley_College_joins_edX.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html b/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html index 020140d281..5e25114d3a 100644 --- a/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html +++ b/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html @@ -65,7 +65,7 @@ fjs.parentNode.insertBefore(js, fjs); -
    +
    From c702bbcd85cf95fdda1752974f6b636abc89ae76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 4 Dec 2012 10:27:23 -0500 Subject: [PATCH 112/133] Modify Wellesley rollover logo --- .../wellesley/wellesley-rollover_160x90.png | Bin 7353 -> 5807 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/lms/static/images/university/wellesley/wellesley-rollover_160x90.png b/lms/static/images/university/wellesley/wellesley-rollover_160x90.png index e670d2aea26876f797e5fa41f4e22ace5314d182..889414c8f41a8a7b4c328ab351268f033c8c30a2 100644 GIT binary patch delta 2351 zcmZ{jX;c!38irlaR2=u+)Et*Ecd9G4%3-hTj(`RZ0cm8rDv>na zJp%${{R+gSTqV?uU0e%rVp6hxO8X?C_J980^!*5FJ<`W)>n~l@%I|Mqvr*Xh>^Um? z_&k-;#NDBwb$Lhz)QIdJZA#NUtgWf_4LyhDJ>~@1=!b`v*I-#Yrp}MoCSazKF`hg% zvOx+inb1_ylr6l;6GSPp8VmT5sN`fJRI|A*k50<}+NqO^)I!><#c)*kX1=R>x2DEbx`fOU-sTlD_&GP$uP|*=uX4D@=XOYF#n}{7=m8Jd_|Ad;-TWVB z1O%J=3r~T8G;_)LjN&CT>ex=NyNpI~*3G$qN4ehXV2_zGDp{z@OJ&UDLeP6Oz)1eWp5kZbxy?Sd53Ua#Iw$ZDh;hLE~;HW98B77kCCWDRy|D`q&A@wdH#3S z-d9v{QOPAu3N~aGe)a0cfL+nRUoh1~gsiD37&Xyg7dE(pox>Qs3Uth0#r3aLfW)m* zLi+{cwr6-s@h^mtJPYn_`R`D@L~xr8w?7(K5t04vniG+sm3uZh1YIkYYO9Zez1itR zTeLwUuqDzj+hI8WqC`ag#B;aexwB6o!?KaO_zUgx7!A)`K+9hxiFNe{*I+hS#0{9K z&bbnd&hRUujyD=wu%LzuZqRk`5WroNmH2Xu#i*F0ZGv;bPP>iueArouC?l;tL9DXn zYsG`9Z)}{%;qm(RxoHI-cb; zvYK2-inHQBkn3wP#75JJzu&!+RDllk5n^K_4JeJw0f^3(f&f$?q2yS^?yp4L+)$o zeYas5VFEF(AEvWh8(H02c4j1l@5(bH?o|h* zra!nHGrx6{Jrq<{8L50?sw0l#88VvUNbPjtS9KrL@UuZDzw&F;Gh=M++FD%xUe9^| zaKK&!GuPG>CapL?Da`z+JMo(3e|sns|7x*q#z*6#KnlJx`l2EG1qPGHPn_s_yC&jX zdta%|YR;hOdFp^!YvLu1T*vGVdsCM1(d0fqZ zxc0@k3%sP9+A*jfyQLt&-dO&xh~q|8koXfjAldGNb&ad_GrFt$+49OV1} z?Z41n;g-*CAYpqKp+Fhlm#b`3aa@3vbj`uJPxYIuVuQh|>y?tTS*nDKf%#kYu>zes;Ku2-h;U45MQv?+Gc*qgufe1wQc<%g=1vm2(6%D3sn8FBhPPReIFIM zuUsfHbXY8N;n#2uM02YWE(wDe38 z@pbCDD9)r?1J%C>4NTWeiuBb3h3D~`Z_RHn z+fS64zza!7JVY<^tU|%KCxjb4yUh#qZHl2|h?m5lc85-1=`c46pvL>^-9v}PMqH*x z^}bn0eWRuD+|9%dfyYy=0~uiR^EXz#rk{)H8UfQWpIX(YX|NWHm5cQyfb<6R!06+n zqi)1crPXql)d09>salY|Mds$%v%9b!Z-lm}ZNC&I)T71v1;e;rbOqQdv%gsE52YVw zFD2^Eg;g#X)*9Cz5X>CNDWpN5HK~xK5$=e7&>xVhS*eVcZ6ZQDXhJe}dBK548~MA> z#tBm6M4k*`Jt%A!Jp#aKc<Ed0vHa6`lk9{N z^BA2|nKD869h_-3W;Rh#-WBT@s_iO#Z|t&I<|DK2vjWcZzV5$wZLVrON!?gle+*`% z@su1M%f6el=vcY;>R04%<9s zHrMu3ovOmR-IIaok1AE(6>;N^PYxgHjx`yQlo`m$!d2w-M5OOLX;9wknMA(m2$jf5 zYx5hs_3^`^5Fj&k@bhr~t*+xU(e9-YDS?a-6@|D6`mJkO9l;X6xSzlFs1*NtvarxZ zqyY}^*vsUxU?Aaqflgfxx+wge@=S(depn22>$`8 CLV2zL delta 3909 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 Date: Tue, 4 Dec 2012 11:52:35 -0500 Subject: [PATCH 113/133] clean up lms tests, and fix export/import --- common/djangoapps/mitxmako/shortcuts.py | 4 -- common/lib/xmodule/xmodule/html_module.py | 15 ++++---- common/lib/xmodule/xmodule/xml_module.py | 13 +++---- common/test/data/full/about/faq.html | 2 +- .../full/problem/H1P3_Poor_Workmanship.xml | 3 +- .../full/problem/Lab_0_Using_the_Tools.xml | 2 +- .../full/problem/Sample_Algebraic_Problem.xml | 2 +- .../full/problem/Sample_Numeric_Problem.xml | 2 +- lms/djangoapps/courseware/tests/tests.py | 37 +++---------------- 9 files changed, 25 insertions(+), 55 deletions(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index 181d3befd5..ebeb0fc180 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging - -log = logging.getLogger("mitx." + __name__) - from django.template import Context from django.http import HttpResponse diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index df77f43a17..c11c7d22e7 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -7,15 +7,14 @@ from lxml import etree from lxml.html import rewrite_links from path import path -from .x_module import XModule from pkg_resources import resource_string -from .xml_module import XmlDescriptor, name_to_pathname -from .editing_module import EditingDescriptor -from .stringify import stringify_children -from .html_checker import check_html -from xmodule.modulestore import Location - from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent +from xmodule.editing_module import EditingDescriptor +from xmodule.html_checker import check_html +from xmodule.modulestore import Location +from xmodule.stringify import stringify_children +from xmodule.x_module import XModule +from xmodule.xml_module import XmlDescriptor, name_to_pathname log = logging.getLogger("mitx.courseware") @@ -164,7 +163,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(self.definition['data']) # .encode('utf-8')) + file.write(self.definition['data'].encode('utf-8')) # write out the relative name relname = path(pathname).basename() diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 91768d8e11..bb5b44c67f 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -1,17 +1,17 @@ -from xmodule.x_module import (XModuleDescriptor, policy_key) -from xmodule.modulestore import Location -from lxml import etree import json import copy import logging -import traceback -from collections import namedtuple -from fs.errors import ResourceNotFoundError import os import sys +from collections import namedtuple +from lxml import etree + +from xmodule.x_module import (XModuleDescriptor, policy_key) +from xmodule.modulestore import Location log = logging.getLogger(__name__) +# assume all XML files are persisted as utf-8. edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True, remove_blank_text=True, encoding='utf-8') @@ -207,7 +207,6 @@ class XmlDescriptor(XModuleDescriptor): definition_xml = cls.load_file(filepath, system.resources_fs, location) - log.info(' read definition XML: %s', definition_xml) definition_metadata = get_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml) definition = cls.definition_from_xml(definition_xml, system) diff --git a/common/test/data/full/about/faq.html b/common/test/data/full/about/faq.html index a5e54c9f15..a173e46753 100644 --- a/common/test/data/full/about/faq.html +++ b/common/test/data/full/about/faq.html @@ -6,7 +6,7 @@

    No - anyone and everyone is welcome to take this course.

  • What textbook should I buy? -

    Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) -- Volume II, which was written specifically for this course.

    +

    Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) — Volume II, which was written specifically for this course.

  • Does Harvard award credentials or reports regarding my work in this course?

    Princeton does not award credentials or issue reports for student work in this course. However, Coursera could maintain a record of your score on the assessments and, with your permission, verify that score for authorized parties.

    diff --git a/common/test/data/full/problem/H1P3_Poor_Workmanship.xml b/common/test/data/full/problem/H1P3_Poor_Workmanship.xml index cf9db4053a..f32b9eb271 100644 --- a/common/test/data/full/problem/H1P3_Poor_Workmanship.xml +++ b/common/test/data/full/problem/H1P3_Poor_Workmanship.xml @@ -78,7 +78,8 @@ So the total heating power in Joe's shop was:
    -No wonder Joe was cold. + +No wonder Joe was cold… diff --git a/common/test/data/full/problem/Lab_0_Using_the_Tools.xml b/common/test/data/full/problem/Lab_0_Using_the_Tools.xml index b5f593c294..c270773da3 100644 --- a/common/test/data/full/problem/Lab_0_Using_the_Tools.xml +++ b/common/test/data/full/problem/Lab_0_Using_the_Tools.xml @@ -94,7 +94,7 @@ scope probes to nodes A, B and C and edit their properties so that the plots will be different colors. Now run a transient analysis for 5ms. Move the mouse over the plot until the marker (a vertical dashed line that follows the mouse when it's over the plot) is at approximately -1.25ms. Please report the measured voltages for nodes A, B and C. +1.25ms. Please report the measured voltages for nodes A, B and C…
    diff --git a/common/test/data/full/problem/Sample_Algebraic_Problem.xml b/common/test/data/full/problem/Sample_Algebraic_Problem.xml index 7bea1cc92e..85b9a2fcc4 100644 --- a/common/test/data/full/problem/Sample_Algebraic_Problem.xml +++ b/common/test/data/full/problem/Sample_Algebraic_Problem.xml @@ -6,7 +6,7 @@ z = "A*x^2 + sqrt(y)" Enter the algebraic expression \(A x^2 + \sqrt{y}\) in the box below. The entry is case sensitive. The product must be indicated with an asterisk, and the exponentation with a caret, so you must write -"A*x^2 + sqrt(y)". +"A*x^2 + sqrt(y)"… diff --git a/common/test/data/full/problem/Sample_Numeric_Problem.xml b/common/test/data/full/problem/Sample_Numeric_Problem.xml index f41881a028..fef9b4648c 100644 --- a/common/test/data/full/problem/Sample_Numeric_Problem.xml +++ b/common/test/data/full/problem/Sample_Numeric_Problem.xml @@ -1,6 +1,6 @@ Enter the numerical value of the expression \(x + y\) where -\(x = 3\) and \(y = 5\). +\(x = 3\) and \(y = 5\)… diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index b4510dcb3c..673f7d968c 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -75,21 +75,8 @@ def xml_store_config(data_dir): } } -def my_xml_store_config(data_dir): - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } -} - TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -# TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) -MY_TEST_DATA_XML_MODULESTORE = my_xml_store_config(TEST_DATA_DIR) REAL_DATA_DIR = settings.GITHUB_REPO_ROOT REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR) @@ -281,25 +268,15 @@ class PageLoader(ActivateLoginTestCase): def check_xml_pages_load(self, course_name, data_dir, modstore): """Make all locations in course load""" print "Checking course {0} in {1}".format(course_name, data_dir) - default_class='xmodule.hidden_module.HiddenDescriptor' # 'xmodule.raw_module.RawDescriptor', + default_class='xmodule.hidden_module.HiddenDescriptor' load_error_modules=True -# load_error_modules=False module_store = XMLModuleStore( data_dir, default_class=default_class, course_dirs=[course_name], load_error_modules=load_error_modules, ) -# for course_id in module_store.modules.keys(): -# for module in module_store.modules[course_id].itervalues(): -# -# if 'data' in module.definition: -# store.update_item(module.location, module.definition['data']) -# if 'children' in module.definition: -# store.update_children(module.location, module.definition['children']) -# # NOTE: It's important to use own_metadata here to avoid writing -# # inherited metadata everywhere. -# store.update_metadata(module.location, dict(module.own_metadata)) + # enroll in the course before trying to access pages courses = module_store.get_courses() self.assertEqual(len(courses), 1) @@ -328,7 +305,6 @@ class PageLoader(ActivateLoginTestCase): all_ok = False num_bad += 1 content = resp.content -# contentlines = content.splitlines() if content.find("this module is temporarily unavailable")>=0: msg = "ERROR unavailable module " all_ok = False @@ -341,16 +317,15 @@ class PageLoader(ActivateLoginTestCase): log.info('Output the content returned for page %s', descriptor.location.url()) log.info('Content returned: %s', content) print msg -# self.assertTrue(all_ok) # fail fast + self.assertTrue(all_ok) # fail fast print "{0}/{1} good".format(n - num_bad, n) log.info( "{0}/{1} good".format(n - num_bad, n)) -# self.assertTrue(all_ok) - self.assertTrue(false) + self.assertTrue(all_ok) -#@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -@override_settings(MODULESTORE=MY_TEST_DATA_XML_MODULESTORE) + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestCoursesLoadTestCase(PageLoader): '''Check that all pages in test courses load properly''' From cde20758f8f874c4adb584c775c804472809f82e Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 4 Dec 2012 13:25:04 -0500 Subject: [PATCH 114/133] cosmetic cleanup --- common/djangoapps/mitxmako/template.py | 3 --- lms/djangoapps/courseware/tests/tests.py | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py index ff83a9be4c..947dc8c1a4 100644 --- a/common/djangoapps/mitxmako/template.py +++ b/common/djangoapps/mitxmako/template.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -log = logging.getLogger("mitx." + __name__) - from django.conf import settings from mako.template import Template as MakoTemplate diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 673f7d968c..e9c3d0583c 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -290,11 +290,11 @@ class PageLoader(ActivateLoginTestCase): for descriptor in module_store.modules[course_id].itervalues(): n += 1 print "Checking ", descriptor.location.url() - log.info('Checking the content returned for page %s', descriptor.location.url()) #print descriptor.__class__, descriptor.location resp = self.client.get(reverse('jump_to', kwargs={'course_id': course_id, 'location': descriptor.location.url()}), follow=True) + # check status codes first msg = str(resp.status_code) if resp.status_code != 200: msg = "ERROR " + msg + ": " + descriptor.location.url() @@ -304,6 +304,8 @@ class PageLoader(ActivateLoginTestCase): msg = "ERROR on redirect from " + descriptor.location.url() all_ok = False num_bad += 1 + + # check content to make sure there were no rendering failures content = resp.content if content.find("this module is temporarily unavailable")>=0: msg = "ERROR unavailable module " @@ -314,8 +316,6 @@ class PageLoader(ActivateLoginTestCase): msg = msg + descriptor.definition['data']['error_msg'] all_ok = False num_bad += 1 - log.info('Output the content returned for page %s', descriptor.location.url()) - log.info('Content returned: %s', content) print msg self.assertTrue(all_ok) # fail fast From 70f35b2e1573f5e96fc8ea64344464043018fa6a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 4 Dec 2012 13:46:32 -0500 Subject: [PATCH 115/133] remove skipped tests that use mongo --- lms/djangoapps/courseware/tests/tests.py | 96 +----------------------- 1 file changed, 2 insertions(+), 94 deletions(-) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index e9c3d0583c..3cb3bc89dd 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -2,10 +2,8 @@ import logging log = logging.getLogger("mitx." + __name__) import json -import os import time -from nose import SkipTest from urlparse import urlsplit, urlunsplit from django.contrib.auth.models import User, Group @@ -44,26 +42,6 @@ def registration(email): '''look up registration object by email''' return Registration.objects.get(user__email=email) - -# A bit of a hack--want mongo modulestore for these tests, until -# jump_to works with the xmlmodulestore or we have an even better solution -# NOTE: this means this test requires mongo to be running. - -def mongo_store_config(data_dir): - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'xmodule', - 'collection': 'modulestore', - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } -} - def xml_store_config(data_dir): return { 'default': { @@ -78,9 +56,6 @@ def xml_store_config(data_dir): TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) -REAL_DATA_DIR = settings.GITHUB_REPO_ROOT -REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR) - class ActivateLoginTestCase(TestCase): '''Check that we can activate and log in''' @@ -228,44 +203,6 @@ class PageLoader(ActivateLoginTestCase): def check_pages_load(self, course_name, data_dir, modstore): - """Make all locations in course load""" - print "Checking course {0} in {1}".format(course_name, data_dir) - import_from_xml(modstore, data_dir, [course_name]) - - # enroll in the course before trying to access pages - courses = modstore.get_courses() - self.assertEqual(len(courses), 1) - course = courses[0] - self.enroll(course) - course_id = course.id - - n = 0 - num_bad = 0 - all_ok = True - for descriptor in modstore.get_items( - Location(None, None, None, None, None)): - n += 1 - print "Checking ", descriptor.location.url() - #print descriptor.__class__, descriptor.location - resp = self.client.get(reverse('jump_to', - kwargs={'course_id': course_id, - 'location': descriptor.location.url()}), follow=True) - msg = str(resp.status_code) - if resp.status_code != 200: - msg = "ERROR " + msg # + ": " + str(resp.request['PATH_INFO']) - all_ok = False - num_bad += 1 - elif resp.redirect_chain[0][1] != 302: - msg = "ERROR " + msg - all_ok = False - num_bad += 1 - print msg -# self.assertTrue(all_ok) # fail fast - - print "{0}/{1} good".format(n - num_bad, n) - self.assertTrue(all_ok) - - def check_xml_pages_load(self, course_name, data_dir, modstore): """Make all locations in course load""" print "Checking course {0} in {1}".format(course_name, data_dir) default_class='xmodule.hidden_module.HiddenDescriptor' @@ -337,10 +274,10 @@ class TestCoursesLoadTestCase(PageLoader): # is there a way to empty the store? def test_toy_course_loads(self): - self.check_xml_pages_load('toy', TEST_DATA_DIR, modulestore()) + self.check_pages_load('toy', TEST_DATA_DIR, modulestore()) def test_full_course_loads(self): - self.check_xml_pages_load('full', TEST_DATA_DIR, modulestore()) + self.check_pages_load('full', TEST_DATA_DIR, modulestore()) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @@ -676,35 +613,6 @@ class TestViewAuth(PageLoader): self.unenroll(self.toy) self.assertTrue(self.try_enroll(self.toy)) - -@override_settings(MODULESTORE=REAL_DATA_MODULESTORE) -class RealCoursesLoadTestCase(PageLoader): - '''Check that all pages in real courses load properly''' - - def setUp(self): - ActivateLoginTestCase.setUp(self) - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - - def test_real_courses_loads(self): - '''See if any real courses are available at the REAL_DATA_DIR. - If they are, check them.''' - - # TODO: Disabled test for now.. Fix once things are cleaned up. - raise SkipTest - # TODO: adjust staticfiles_dirs - if not os.path.isdir(REAL_DATA_DIR): - # No data present. Just pass. - return - - courses = [course_dir for course_dir in os.listdir(REAL_DATA_DIR) - if os.path.isdir(REAL_DATA_DIR / course_dir)] - for course in courses: - self.check_pages_load(course, REAL_DATA_DIR, modulestore()) - - - # ========= TODO: check ajax interaction here too? - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestCourseGrader(PageLoader): """Check that a course gets graded properly""" From cee0781a200c1e5ff2f1d05cf8b0913691fdb197 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 4 Dec 2012 15:51:36 -0500 Subject: [PATCH 116/133] Don't cap points coming back from CodeResponses -- let them be more than max_points if they want to award extra credit. --- common/lib/capa/capa/responsetypes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 20e7c43577..82d1e4770b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1309,8 +1309,6 @@ class CodeResponse(LoncapaResponse): # Sanity check on returned points if points < 0: points = 0 - elif points > self.maxpoints[self.answer_id]: - points = self.maxpoints[self.answer_id] # Queuestate is consumed oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) From d4cdd5b4855f8469253d1616f7290862aceaba4d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 09:38:47 -0500 Subject: [PATCH 117/133] Make jenkins talk to the github status api --- jenkins/base.sh | 12 ++++++++++++ jenkins/test_edge.sh | 7 ++++++- jenkins/test_lms.sh | 10 +++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 jenkins/base.sh diff --git a/jenkins/base.sh b/jenkins/base.sh new file mode 100644 index 0000000000..c7175e6e52 --- /dev/null +++ b/jenkins/base.sh @@ -0,0 +1,12 @@ + +function github_status { + gcli status create mitx mitx $GIT_COMMIT \ + --params=$1 \ + target_url:$BUILD_URL \ + description:"Build #$BUILD_NUMBER is running" \ + -f csv +} + +function github_mark_failed_on_exit { + trap '[ $? == "0" ] || github_status state:failed' EXIT +} \ No newline at end of file diff --git a/jenkins/test_edge.sh b/jenkins/test_edge.sh index ccca91fc37..71405d663f 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test_edge.sh @@ -3,6 +3,9 @@ set -e set -x +github_mark_failed_on_exit +github_status state:pending + # Reset the submodule, in case it changed git submodule foreach 'git reset --hard HEAD' @@ -26,4 +29,6 @@ rake phantomjs_jasmine_cms || true rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] -rake autodeploy_properties \ No newline at end of file +rake autodeploy_properties + +github_status state:success diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index a7f8555f81..69fbf50dfa 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -3,6 +3,12 @@ set -e set -x +. base.sh + +github_mark_failed_on_exit +github_status state:pending + + # Reset the submodule, in case it changed git submodule foreach 'git reset --hard HEAD' @@ -24,4 +30,6 @@ rake phantomjs_jasmine_lms || true rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] -rake autodeploy_properties \ No newline at end of file +rake autodeploy_properties + +github_status state:success From 724cba193af06d3aa890dc602a3e56d55713491c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 09:43:52 -0500 Subject: [PATCH 118/133] Source the base jenkins configuration before running tests --- jenkins/test_edge.sh | 2 ++ jenkins/test_lms.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jenkins/test_edge.sh b/jenkins/test_edge.sh index 71405d663f..51d24bc6f6 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test_edge.sh @@ -3,6 +3,8 @@ set -e set -x +source base.sh + github_mark_failed_on_exit github_status state:pending diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index 69fbf50dfa..13d7ffb885 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -3,7 +3,7 @@ set -e set -x -. base.sh +source base.sh github_mark_failed_on_exit github_status state:pending From 86579efc5bce7799089dcb64b1bb9731a3cad12c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 09:59:21 -0500 Subject: [PATCH 119/133] Source jenkins/base.sh with the correct path --- jenkins/base.sh | 1 + jenkins/test_edge.sh | 2 +- jenkins/test_lms.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jenkins/base.sh b/jenkins/base.sh index c7175e6e52..1b28028541 100644 --- a/jenkins/base.sh +++ b/jenkins/base.sh @@ -1,3 +1,4 @@ +#! /bin/bash function github_status { gcli status create mitx mitx $GIT_COMMIT \ diff --git a/jenkins/test_edge.sh b/jenkins/test_edge.sh index 51d24bc6f6..cb6678c04c 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test_edge.sh @@ -3,7 +3,7 @@ set -e set -x -source base.sh +source jenkins/base.sh github_mark_failed_on_exit github_status state:pending diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index 13d7ffb885..257609e80d 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -3,7 +3,7 @@ set -e set -x -source base.sh +source jenkins/base.sh github_mark_failed_on_exit github_status state:pending From 3f5187401d9e2d48b2177d7672c0c313a979815b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 10:29:13 -0500 Subject: [PATCH 120/133] Change message with state --- jenkins/base.sh | 4 ++-- jenkins/test_edge.sh | 4 ++-- jenkins/test_lms.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jenkins/base.sh b/jenkins/base.sh index 1b28028541..d5782fa732 100644 --- a/jenkins/base.sh +++ b/jenkins/base.sh @@ -4,10 +4,10 @@ function github_status { gcli status create mitx mitx $GIT_COMMIT \ --params=$1 \ target_url:$BUILD_URL \ - description:"Build #$BUILD_NUMBER is running" \ + description:"Build #$BUILD_NUMBER $2" \ -f csv } function github_mark_failed_on_exit { - trap '[ $? == "0" ] || github_status state:failed' EXIT + trap '[ $? == "0" ] || github_status state:failed "failed"' EXIT } \ No newline at end of file diff --git a/jenkins/test_edge.sh b/jenkins/test_edge.sh index cb6678c04c..19ff5ecbde 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test_edge.sh @@ -6,7 +6,7 @@ set -x source jenkins/base.sh github_mark_failed_on_exit -github_status state:pending +github_status state:pending "is running" # Reset the submodule, in case it changed git submodule foreach 'git reset --hard HEAD' @@ -33,4 +33,4 @@ rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] rake autodeploy_properties -github_status state:success +github_status state:success "passed" diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index 257609e80d..9410845cdb 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -6,7 +6,7 @@ set -x source jenkins/base.sh github_mark_failed_on_exit -github_status state:pending +github_status state:pending "is running" # Reset the submodule, in case it changed @@ -32,4 +32,4 @@ rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] rake autodeploy_properties -github_status state:success +github_status state:success "passed" From d4ec5946d48c6d4f732d59b747fb1e88a4812fc4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 10:37:44 -0500 Subject: [PATCH 121/133] Mark failed state correctly --- jenkins/base.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/base.sh b/jenkins/base.sh index d5782fa732..274c8d3027 100644 --- a/jenkins/base.sh +++ b/jenkins/base.sh @@ -9,5 +9,5 @@ function github_status { } function github_mark_failed_on_exit { - trap '[ $? == "0" ] || github_status state:failed "failed"' EXIT + trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT } \ No newline at end of file From 5a3cdc11cd2aaf7bb2602b260fac6168e06f9534 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 10:41:24 -0500 Subject: [PATCH 122/133] Only mark github commit status when full test suite passes --- jenkins/test_lms.sh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index 9410845cdb..5a2f2a0924 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -3,12 +3,6 @@ set -e set -x -source jenkins/base.sh - -github_mark_failed_on_exit -github_status state:pending "is running" - - # Reset the submodule, in case it changed git submodule foreach 'git reset --hard HEAD' @@ -31,5 +25,3 @@ rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] rake autodeploy_properties - -github_status state:success "passed" From e2ed7d1c4e87a3af09099a31785532a56055e695 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 10:56:24 -0500 Subject: [PATCH 123/133] Move to a single test.sh file that only runs the relevant tests for a particular branch in the repo. In particular, don't run cms tests on branches derived from master --- jenkins/{test_edge.sh => test.sh} | 6 ++++-- jenkins/test_lms.sh | 27 --------------------------- 2 files changed, 4 insertions(+), 29 deletions(-) rename jenkins/{test_edge.sh => test.sh} (77%) delete mode 100755 jenkins/test_lms.sh diff --git a/jenkins/test_edge.sh b/jenkins/test.sh similarity index 77% rename from jenkins/test_edge.sh rename to jenkins/test.sh index 19ff5ecbde..ed65fd2618 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test.sh @@ -22,12 +22,14 @@ yes w | pip install -q -r requirements.txt rake clobber TESTS_FAILED=0 -rake test_cms[false] || TESTS_FAILED=1 +# Don't run the studio tests until feature/cale/cms-master is merged in +# rake test_cms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1 rake phantomjs_jasmine_lms || true -rake phantomjs_jasmine_cms || true +# Don't run the studio tests until feature/cale/cms-master is merged in +# rake phantomjs_jasmine_cms || true rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh deleted file mode 100755 index 5a2f2a0924..0000000000 --- a/jenkins/test_lms.sh +++ /dev/null @@ -1,27 +0,0 @@ -#! /bin/bash - -set -e -set -x - -# Reset the submodule, in case it changed -git submodule foreach 'git reset --hard HEAD' - -# Set the IO encoding to UTF-8 so that askbot will start -export PYTHONIOENCODING=UTF-8 - -GIT_BRANCH=${GIT_BRANCH/HEAD/master} - -pip install -q -r pre-requirements.txt -pip install -q -r test-requirements.txt -yes w | pip install -q -r requirements.txt - -rake clobber -TESTS_FAILED=0 -rake test_lms[false] || TESTS_FAILED=1 -rake test_common/lib/capa || TESTS_FAILED=1 -rake test_common/lib/xmodule || TESTS_FAILED=1 -rake phantomjs_jasmine_lms || true -rake coverage:xml coverage:html - -[ $TESTS_FAILED == '0' ] -rake autodeploy_properties From 38671fc83c3d00f8908cd6aa9e4e2a0e377880bf Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 11:01:58 -0500 Subject: [PATCH 124/133] Move files from base.sh to test.sh --- jenkins/base.sh | 13 ------------- jenkins/test.sh | 12 +++++++++++- 2 files changed, 11 insertions(+), 14 deletions(-) delete mode 100644 jenkins/base.sh diff --git a/jenkins/base.sh b/jenkins/base.sh deleted file mode 100644 index 274c8d3027..0000000000 --- a/jenkins/base.sh +++ /dev/null @@ -1,13 +0,0 @@ -#! /bin/bash - -function github_status { - gcli status create mitx mitx $GIT_COMMIT \ - --params=$1 \ - target_url:$BUILD_URL \ - description:"Build #$BUILD_NUMBER $2" \ - -f csv -} - -function github_mark_failed_on_exit { - trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT -} \ No newline at end of file diff --git a/jenkins/test.sh b/jenkins/test.sh index ed65fd2618..8a96024785 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -3,7 +3,17 @@ set -e set -x -source jenkins/base.sh +function github_status { + gcli status create mitx mitx $GIT_COMMIT \ + --params=$1 \ + target_url:$BUILD_URL \ + description:"Build #$BUILD_NUMBER $2" \ + -f csv +} + +function github_mark_failed_on_exit { + trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT +} github_mark_failed_on_exit github_status state:pending "is running" From 3b75bbdc6fb138b9997be59d7809417002880cd0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 11:18:38 -0500 Subject: [PATCH 125/133] Actually run cms tests on cms master --- jenkins/test.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index 8a96024785..5d33fd5612 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -32,14 +32,12 @@ yes w | pip install -q -r requirements.txt rake clobber TESTS_FAILED=0 -# Don't run the studio tests until feature/cale/cms-master is merged in -# rake test_cms[false] || TESTS_FAILED=1 +rake test_cms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1 rake phantomjs_jasmine_lms || true -# Don't run the studio tests until feature/cale/cms-master is merged in -# rake phantomjs_jasmine_cms || true +rake phantomjs_jasmine_cms || true rake coverage:xml coverage:html [ $TESTS_FAILED == '0' ] From 3e568667fc2d046194335492d2b57127b4e30ad7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 11:24:25 -0500 Subject: [PATCH 126/133] Fix merge badness --- common/lib/xmodule/xmodule/x_module.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 5009a4e71c..88f4c3c48a 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -435,13 +435,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): # cdodge: this is a list of metadata names which are 'system' metadata # and should not be edited by an end-user -<<<<<<< HEAD system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft'] - -======= - system_metadata_fields = [ 'data_dir' ] ->>>>>>> origin/master # A list of descriptor attributes that must be equal for the descriptors to # be equal equality_attributes = ('definition', 'metadata', 'location', From 47ed02c9ea2ef4e55478317184cb26a831f9413c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 11:30:21 -0500 Subject: [PATCH 127/133] Remove more merge badness --- lms/djangoapps/courseware/views.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 3908f09706..4472eee7fa 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -404,13 +404,8 @@ def course_about(request, course_id): show_courseware_link = (has_access(request.user, course, 'load') or settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION')) -<<<<<<< HEAD - return render_to_response('portal/course_about.html', - { 'course': course, -======= return render_to_response('courseware/course_about.html', {'course': course, ->>>>>>> origin/master 'registered': registered, 'course_target': course_target, 'show_courseware_link' : show_courseware_link}) From b4793b3277ccd381068ce24702cc513e0d2ece2f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 7 Dec 2012 12:02:29 -0500 Subject: [PATCH 128/133] Add an implementation of get_items to xml module store --- common/lib/xmodule/xmodule/modulestore/xml.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index a6ff6ab26f..b7f1b0da02 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -513,6 +513,18 @@ class XMLModuleStore(ModuleStoreBase): raise NotImplementedError("XMLModuleStores can't guarantee that definitions" " are unique. Use get_instance.") + def get_items(self, location, depth=0): + items = [] + for _, modules in self.modules.iteritems(): + for mod_loc, module in modules.iteritems(): + + # Locations match if each value in `location` is None or if the value from `location` + # matches the value from `mod_loc` + if all(goal is None or goal == value for goal, value in zip(location, mod_loc)): + items.append(module) + + return items + def get_courses(self, depth=0): """ From 0e3a022086565a57f5657c3c82f1e3e0ab1ec69d Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 11 Dec 2012 11:21:08 -0500 Subject: [PATCH 129/133] fix broken tests from master -> CMS-master merge. Namely the change to unicode strings when reading files off disk. Also there was some work collisions on course_loads which removed Mongo testing. Make it so that explicitly test both XMLFilestores as well as Mongo --- common/lib/xmodule/xmodule/course_module.py | 3 +- lms/djangoapps/courseware/tests/tests.py | 71 ++++++++++++++------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index dbfbc10d73..a8f95c56bb 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -192,7 +192,8 @@ class CourseDescriptor(SequenceDescriptor): instance = super(CourseDescriptor, cls).from_xml(xml_data, system, org, course) # bleh, have to parse the XML here to just pull out the url_name attribute - course_file = StringIO(xml_data) + # I don't think it's stored anywhere in the instance. + course_file = StringIO(xml_data.encode('ascii','ignore')) xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot() policy_dir = None diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 77e1b049b1..f6f6f93c04 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -14,6 +14,8 @@ from django.core.urlresolvers import reverse from override_settings import override_settings import xmodule.modulestore.django +from xmodule.modulestore.mongo import MongoModuleStore + # Need access to internal func to put users in the right group from courseware import grades @@ -74,6 +76,7 @@ def xml_store_config(data_dir): TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) class ActivateLoginTestCase(TestCase): '''Check that we can activate and log in''' @@ -221,17 +224,9 @@ class PageLoader(ActivateLoginTestCase): .format(resp.status_code, url, code)) - def check_pages_load(self, course_name, data_dir, modstore): + def check_pages_load(self, module_store): """Make all locations in course load""" - print "Checking course {0} in {1}".format(course_name, data_dir) - default_class='xmodule.hidden_module.HiddenDescriptor' - load_error_modules=True - module_store = XMLModuleStore( - data_dir, - default_class=default_class, - course_dirs=[course_name], - load_error_modules=load_error_modules, - ) + # enroll in the course before trying to access pages courses = module_store.get_courses() @@ -243,7 +238,7 @@ class PageLoader(ActivateLoginTestCase): n = 0 num_bad = 0 all_ok = True - for descriptor in modstore.get_items( + for descriptor in module_store.get_items( Location(None, None, None, None, None)): n += 1 @@ -274,6 +269,8 @@ class PageLoader(ActivateLoginTestCase): msg = "ERROR " + msg all_ok = False num_bad += 1 + elif descriptor.location.category == 'custom_tag_template': + pass else: #print descriptor.__class__, descriptor.location resp = self.client.get(reverse('jump_to', @@ -282,13 +279,9 @@ class PageLoader(ActivateLoginTestCase): msg = str(resp.status_code) if resp.status_code != 302: - # cdodge: we're adding 'custom_tag_template' which is the Mako template used to render - # the custom tag. We can't 'jump-to' this module. Unfortunately, we also can't test render - # it easily - if descriptor.location.category not in ['custom_tag_template'] or resp.status_code != 404: - msg = "ERROR " + msg - all_ok = False - num_bad += 1 + msg = "ERROR " + msg + all_ok = False + num_bad += 1 print msg self.assertTrue(all_ok) # fail fast @@ -299,21 +292,51 @@ class PageLoader(ActivateLoginTestCase): @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase(PageLoader): +class TestCoursesLoadTestCase_XmlModulestore(PageLoader): '''Check that all pages in test courses load properly''' def setUp(self): ActivateLoginTestCase.setUp(self) xmodule.modulestore.django._MODULESTORES = {} -# xmodule.modulestore.django.modulestore().collection.drop() -# store = xmodule.modulestore.django.modulestore() - # is there a way to empty the store? def test_toy_course_loads(self): - self.check_pages_load('toy', TEST_DATA_DIR, modulestore()) + module_store = XMLModuleStore( + TEST_DATA_DIR, + default_class='xmodule.hidden_module.HiddenDescriptor', + course_dirs=['toy'], + load_error_modules=True, + ) + + self.check_pages_load(module_store) def test_full_course_loads(self): - self.check_pages_load('full', TEST_DATA_DIR, modulestore()) + module_store = XMLModuleStore( + TEST_DATA_DIR, + default_class='xmodule.hidden_module.HiddenDescriptor', + course_dirs=['full'], + load_error_modules=True, + ) + self.check_pages_load(module_store) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestCoursesLoadTestCase_MongoModulestore(PageLoader): + '''Check that all pages in test courses load properly''' + + def setUp(self): + ActivateLoginTestCase.setUp(self) + xmodule.modulestore.django._MODULESTORES = {} + modulestore().collection.drop() + + def test_toy_course_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['toy']) + self.check_pages_load(module_store) + + def test_full_course_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['full']) + self.check_pages_load(module_store) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) From 7229a9cdaf459d6d99a63ebed843e8680fd7c7e7 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 3 Jan 2013 10:43:03 -0500 Subject: [PATCH 130/133] perform a new merge from master, resolve conflicts --- .gitmodules | 3 - cms/.coveragerc | 2 + common/djangoapps/student/models.py | 25 +- common/djangoapps/student/views.py | 21 +- .../track/migrations/0001_initial.py | 48 ++ ...t__chg_field_trackinglog_event_type__ch.py | 51 ++ .../djangoapps/track/migrations/__init__.py | 1 + common/djangoapps/track/models.py | 5 +- common/djangoapps/track/views.py | 4 +- common/djangoapps/util/json_request.py | 5 + common/lib/capa/.coveragerc | 1 + common/lib/capa/capa/capa_problem.py | 4 +- common/lib/capa/capa/inputtypes.py | 50 ++ common/lib/capa/capa/responsetypes.py | 383 ++++++++++++- .../capa/capa/templates/openendedinput.html | 32 ++ common/lib/capa/capa/util.py | 22 + common/lib/capa/capa/xqueue_interface.py | 7 +- common/lib/xmodule/.coveragerc | 1 + common/lib/xmodule/xmodule/capa_module.py | 5 + common/lib/xmodule/xmodule/course_module.py | 15 +- .../lib/xmodule/xmodule/css/capa/display.scss | 39 +- .../xmodule/xmodule/js/src/capa/schematic.js | 10 +- .../xmodule/js/src/video/display.coffee | 2 + .../js/src/video/display/video_player.coffee | 21 +- .../xmodule/xmodule/modulestore/__init__.py | 13 + .../xmodule/xmodule/self_assessment_module.py | 8 + common/lib/xmodule/xmodule/seq_module.py | 2 +- common/lib/xmodule/xmodule/video_module.py | 31 +- common/lib/xmodule/xmodule/x_module.py | 6 +- .../static/coffee/src/discussion/utils.coffee | 5 +- .../static/sass/bourbon/css3/_box-sizing.scss | 2 +- create-dev-env.sh | 132 +++-- doc/development.md | 10 + doc/xml-format.md | 8 +- install-system-req.sh | 108 ++++ lms/.coveragerc | 2 + lms/djangoapps/courseware/access.py | 3 +- lms/djangoapps/courseware/courses.py | 46 +- lms/djangoapps/courseware/module_render.py | 11 +- lms/djangoapps/courseware/tabs.py | 11 +- lms/djangoapps/courseware/tests/tests.py | 133 ++--- lms/djangoapps/courseware/views.py | 22 +- .../django_comment_client/models.py | 15 + lms/djangoapps/django_comment_client/utils.py | 14 +- lms/djangoapps/instructor/grading.py | 25 + .../instructor/staff_grading_service.py | 390 ++++++++++++++ lms/djangoapps/instructor/tests.py | 149 +++++- lms/djangoapps/instructor/views.py | 33 +- lms/envs/aws.py | 2 + lms/envs/common.py | 21 +- lms/envs/dev.py | 8 +- lms/envs/test.py | 12 +- lms/static/admin/css/ie.css | 63 --- lms/static/coffee/src/main.coffee | 10 + .../src/staff_grading/staff_grading.coffee | 404 ++++++++++++++ .../src/staff_grading/test_grading.html | 45 ++ lms/static/images/logo-edx-support.png | Bin 0 -> 3288 bytes .../releases/georgetown-seal_240x180.png | Bin 0 -> 32625 bytes .../georgetown/georgetown-cover_2025x550.jpg | Bin 0 -> 317367 bytes .../georgetown-rollover_350x150.png | Bin 0 -> 15087 bytes .../university/georgetown/georgetown.png | Bin 0 -> 15087 bytes .../university/ut/ut-rollover_350x150.png | Bin 0 -> 11746 bytes .../wellesley/wellesley-rollover_350x150.png | Bin 0 -> 5608 bytes lms/static/js/form.ext.js | 2 +- lms/static/sass/course.scss | 1 + lms/static/sass/course/_staff_grading.scss | 86 +++ .../sass/course/courseware/_courseware.scss | 14 +- lms/static/sass/ie.scss | 28 + lms/static/sass/multicourse/_home.scss | 21 +- .../sass/multicourse/_press_release.scss | 4 + lms/static/sass/shared/_course_object.scss | 42 ++ lms/static/scripts/boxsizing.htc | 504 ++++++++++++++++++ lms/templates/accounts_login.html | 92 ++++ lms/templates/course.html | 3 + lms/templates/courseware/courses.html | 18 +- lms/templates/feed.rss | 20 +- lms/templates/index.html | 44 +- lms/templates/instructor/staff_grading.html | 90 ++++ lms/templates/open_ended_error.html | 12 + lms/templates/open_ended_feedback.html | 16 + lms/templates/self_assessment_prompt.html | 2 +- lms/templates/static_templates/faq.html | 71 +-- lms/templates/static_templates/jobs.html | 133 ++++- .../press_releases/Georgetown_joins_edX.html | 73 +++ .../Spring_2013_course_announcements.html | 75 +++ .../university_profile/georgetownx.html | 24 + lms/templates/video.html | 4 +- lms/urls.py | 22 +- rakefile | 18 +- requirements.txt | 100 ++-- 90 files changed, 3529 insertions(+), 491 deletions(-) delete mode 100644 .gitmodules create mode 100644 common/djangoapps/track/migrations/0001_initial.py create mode 100644 common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py create mode 100644 common/djangoapps/track/migrations/__init__.py create mode 100644 common/lib/capa/capa/templates/openendedinput.html create mode 100755 install-system-req.sh create mode 100644 lms/djangoapps/instructor/grading.py create mode 100644 lms/djangoapps/instructor/staff_grading_service.py delete mode 100644 lms/static/admin/css/ie.css create mode 100644 lms/static/coffee/src/staff_grading/staff_grading.coffee create mode 100644 lms/static/coffee/src/staff_grading/test_grading.html create mode 100644 lms/static/images/logo-edx-support.png create mode 100644 lms/static/images/press/releases/georgetown-seal_240x180.png create mode 100644 lms/static/images/university/georgetown/georgetown-cover_2025x550.jpg create mode 100644 lms/static/images/university/georgetown/georgetown-rollover_350x150.png create mode 100644 lms/static/images/university/georgetown/georgetown.png create mode 100644 lms/static/images/university/ut/ut-rollover_350x150.png create mode 100644 lms/static/images/university/wellesley/wellesley-rollover_350x150.png create mode 100644 lms/static/sass/course/_staff_grading.scss create mode 100644 lms/static/scripts/boxsizing.htc create mode 100644 lms/templates/accounts_login.html create mode 100644 lms/templates/instructor/staff_grading.html create mode 100644 lms/templates/open_ended_error.html create mode 100644 lms/templates/open_ended_feedback.html create mode 100644 lms/templates/static_templates/press_releases/Georgetown_joins_edX.html create mode 100644 lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html create mode 100644 lms/templates/university_profile/georgetownx.html diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3c8158cf80..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "common/test/phantom-jasmine"] - path = common/test/phantom-jasmine - url = https://github.com/jcarver989/phantom-jasmine.git diff --git a/cms/.coveragerc b/cms/.coveragerc index 42638feb8f..9b1e59d670 100644 --- a/cms/.coveragerc +++ b/cms/.coveragerc @@ -2,11 +2,13 @@ [run] data_file = reports/cms/.coverage source = cms +omit = cms/envs/*, cms/manage.py [report] ignore_errors = True [html] +title = CMS Python Test Coverage Report directory = reports/cms/cover [xml] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 5975853a21..4932e579a7 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -36,7 +36,7 @@ file and check it in at the same time as your model changes. To do that, 3. Add the migration file created in mitx/common/djangoapps/student/migrations/ """ from datetime import datetime -from hashlib import sha1 +import hashlib import json import logging import uuid @@ -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__) @@ -197,14 +196,13 @@ def unique_id_for_user(user): """ Return a unique id for a user, suitable for inserting into e.g. personalized survey links. - - Currently happens to be implemented as a sha1 hash of the username - (and thus assumes that usernames don't change). """ - # Using the user id as the salt because it's sort of random, and is already - # in the db. - salt = str(user.id) - return sha1(salt + user.username).hexdigest() + # include the secret key as a salt, and to make the ids unique accross + # different LMS installs. + h = hashlib.md5() + h.update(settings.SECRET_KEY) + h.update(str(user.id)) + return h.hexdigest() ## TODO: Should be renamed to generic UserGroup, and possibly @@ -263,15 +261,6 @@ class CourseEnrollment(models.Model): return "[CourseEnrollment] %s: %s (%s)" % (self.user, 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) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 44877ef597..06c59d7937 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -40,7 +40,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from datetime import date from collections import namedtuple -from courseware.courses import get_courses_by_university +from courseware.courses import get_courses from courseware.access import has_access from statsd import statsd @@ -74,16 +74,21 @@ def index(request, extra_context={}, user=None): domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False if domain==False: # do explicit check, because domain=None is valid domain = request.META.get('HTTP_HOST') - universities = get_courses_by_university(None, - domain=domain) + + courses = get_courses(None, domain=domain) + + # Sort courses by how far are they from they start day + key = lambda course: course.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) # Get the 3 most recent news top_news = _get_news(top=3) - context = {'universities': universities, 'news': top_news} + context = {'courses': courses, 'news': top_news} context.update(extra_context) return render_to_response('index.html', context) + def course_from_id(course_id): """Return the CourseDescriptor corresponding to this course_id""" course_loc = CourseDescriptor.id_to_location(course_id) @@ -333,6 +338,14 @@ def change_enrollment(request): return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'} +@ensure_csrf_cookie +def accounts_login(request, error=""): + + + return render_to_response('accounts_login.html', { 'error': error }) + + + # Need different levels of logging @ensure_csrf_cookie def login_user(request, error=""): diff --git a/common/djangoapps/track/migrations/0001_initial.py b/common/djangoapps/track/migrations/0001_initial.py new file mode 100644 index 0000000000..0546203cf8 --- /dev/null +++ b/common/djangoapps/track/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# -*- 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 'TrackingLog' + db.create_table('track_trackinglog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('dtcreated', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('username', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('ip', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('event_source', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('event_type', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('event', self.gf('django.db.models.fields.TextField')(blank=True)), + ('agent', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)), + ('page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('track', ['TrackingLog']) + + + def backwards(self, orm): + # Deleting model 'TrackingLog' + db.delete_table('track_trackinglog') + + + models = { + 'track.trackinglog': { + 'Meta': {'object_name': 'TrackingLog'}, + 'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), + 'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'event_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'page': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}) + } + } + + complete_apps = ['track'] \ No newline at end of file diff --git a/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py b/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py new file mode 100644 index 0000000000..4c73aa3bfd --- /dev/null +++ b/common/djangoapps/track/migrations/0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch.py @@ -0,0 +1,51 @@ +# -*- 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 'TrackingLog.host' + db.add_column('track_trackinglog', 'host', + self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True), + keep_default=False) + + + # Changing field 'TrackingLog.event_type' + db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=512)) + + # Changing field 'TrackingLog.page' + db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=512, null=True)) + + def backwards(self, orm): + # Deleting field 'TrackingLog.host' + db.delete_column('track_trackinglog', 'host') + + + # Changing field 'TrackingLog.event_type' + db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=32)) + + # Changing field 'TrackingLog.page' + db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)) + + models = { + 'track.trackinglog': { + 'Meta': {'object_name': 'TrackingLog'}, + 'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), + 'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'event_type': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'page': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}) + } + } + + complete_apps = ['track'] \ No newline at end of file diff --git a/common/djangoapps/track/migrations/__init__.py b/common/djangoapps/track/migrations/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/djangoapps/track/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/common/djangoapps/track/models.py b/common/djangoapps/track/models.py index 401fa2832f..dfdf7a0558 100644 --- a/common/djangoapps/track/models.py +++ b/common/djangoapps/track/models.py @@ -7,11 +7,12 @@ class TrackingLog(models.Model): username = models.CharField(max_length=32,blank=True) ip = models.CharField(max_length=32,blank=True) event_source = models.CharField(max_length=32) - event_type = models.CharField(max_length=32,blank=True) + event_type = models.CharField(max_length=512,blank=True) event = models.TextField(blank=True) agent = models.CharField(max_length=256,blank=True) - page = models.CharField(max_length=32,blank=True,null=True) + page = models.CharField(max_length=512,blank=True,null=True) time = models.DateTimeField('event time') + host = models.CharField(max_length=64,blank=True) def __unicode__(self): s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source, diff --git a/common/djangoapps/track/views.py b/common/djangoapps/track/views.py index 434e75a63f..54bd476799 100644 --- a/common/djangoapps/track/views.py +++ b/common/djangoapps/track/views.py @@ -17,7 +17,7 @@ from track.models import TrackingLog log = logging.getLogger("tracking") -LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time'] +LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host'] def log_event(event): event_str = json.dumps(event) @@ -58,6 +58,7 @@ def user_track(request): "agent": agent, "page": request.GET['page'], "time": datetime.datetime.utcnow().isoformat(), + "host": request.META['SERVER_NAME'], } log_event(event) return HttpResponse('success') @@ -83,6 +84,7 @@ def server_track(request, event_type, event, page=None): "agent": agent, "page": page, "time": datetime.datetime.utcnow().isoformat(), + "host": request.META['SERVER_NAME'], } if event_type.startswith("/event_logs") and request.user.is_staff: # don't log diff --git a/common/djangoapps/util/json_request.py b/common/djangoapps/util/json_request.py index 169a7e3fb4..9458bff858 100644 --- a/common/djangoapps/util/json_request.py +++ b/common/djangoapps/util/json_request.py @@ -4,6 +4,11 @@ import json def expect_json(view_function): + """ + View decorator for simplifying handing of requests that expect json. If the request's + CONTENT_TYPE is application/json, parses the json dict from request.body, and updates + request.POST with the contents. + """ @wraps(view_function) def expect_json_with_cloned_request(request, *args, **kwargs): # cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information diff --git a/common/lib/capa/.coveragerc b/common/lib/capa/.coveragerc index 6af3218f75..149a4c860a 100644 --- a/common/lib/capa/.coveragerc +++ b/common/lib/capa/.coveragerc @@ -7,6 +7,7 @@ source = common/lib/capa ignore_errors = True [html] +title = Capa Python Test Coverage Report directory = reports/common/lib/capa/cover [xml] diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index db42fb698a..2eaa0e4286 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -53,7 +53,7 @@ response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__]) solution_tags = ['solution'] # these get captured as student responses -response_properties = ["codeparam", "responseparam", "answer"] +response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] # special problem tags which should be turned into innocuous HTML html_transforms = {'problem': {'tag': 'div'}, @@ -72,7 +72,7 @@ global_context = {'random': random, 'miller': chem.miller} # These should be removed from HTML output, including all subelements -html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] +html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"] log = logging.getLogger('mitx.' + __name__) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index ec1cda83c7..73056bc09e 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -733,3 +733,53 @@ class ChemicalEquationInput(InputTypeBase): return {'previewer': '/static/js/capa/chemical_equation_preview.js',} 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 5c04ee5e13..2a58ee1a01 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -8,21 +8,23 @@ Used by capa_problem.py ''' # standard library imports +import abc import cgi +import hashlib import inspect import json import logging import numbers import numpy +import os import random import re import requests -import traceback -import hashlib -import abc -import os import subprocess +import traceback import xml.sax.saxutils as saxutils + +from collections import namedtuple from shapely.geometry import Point, MultiPoint # specific library imports @@ -1105,6 +1107,15 @@ class SymbolicResponse(CustomResponse): #----------------------------------------------------------------------------- +""" +valid: Flag indicating valid score_msg format (Boolean) +correct: Correctness of submission (Boolean) +score: Points to be assigned (numeric, can be float) +msg: Message from grader to display to student (string) +""" +ScoreMessage = namedtuple('ScoreMessage', + ['valid', 'correct', 'points', 'msg']) + class CodeResponse(LoncapaResponse): """ @@ -1144,7 +1155,7 @@ class CodeResponse(LoncapaResponse): else: self._parse_coderesponse_xml(codeparam) - def _parse_coderesponse_xml(self,codeparam): + def _parse_coderesponse_xml(self, codeparam): ''' Parse the new CodeResponse XML format. When successful, sets: self.initial_display @@ -1156,17 +1167,9 @@ class CodeResponse(LoncapaResponse): grader_payload = grader_payload.text if grader_payload is not None else '' self.payload = {'grader_payload': grader_payload} - answer_display = codeparam.find('answer_display') - if answer_display is not None: - self.answer = answer_display.text - else: - self.answer = 'No answer provided.' - - initial_display = codeparam.find('initial_display') - if initial_display is not None: - self.initial_display = initial_display.text - else: - self.initial_display = '' + self.initial_display = find_with_default(codeparam, 'initial_display', '') + self.answer = find_with_default(codeparam, 'answer_display', + 'No answer provided.') def _parse_externalresponse_xml(self): ''' @@ -1732,9 +1735,9 @@ class ImageResponse(LoncapaResponse): Regions is list of lists [region1, region2, region3, ...] where regionN is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. - + If there is only one region in the list, simpler notation can be used: - regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly + regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly setting outer list) Returns: @@ -1816,6 +1819,347 @@ 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' + 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) + + # The openendedparam tag encapsulates all grader settings + oeparam = self.xml.find('openendedparam') + prompt = self.xml.find('prompt') + rubric = self.xml.find('openendedrubric') + + 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) + parsed_grader_payload.update({ + 'location' : self.system.location, + 'course_id' : self.system.course_id, + 'prompt' : prompt_string, + 'rubric' : rubric_string, + }) + updated_grader_payload = json.dumps(parsed_grader_payload) + + self.payload = {'grader_payload': updated_grader_payload} + + self.initial_display = find_with_default(oeparam, 'initial_display', '') + self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') + try: + self.max_score = int(find_with_default(oeparam, 'max_score', 1)) + except ValueError: + self.max_score = 1 + + 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 format_feedback(feedback_type, value): + return """ +
    + {value} +
    + """.format(feedback_type=feedback_type, value=value) + + # 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']: + 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) + return u"\n".join(format_feedback(k, v) for k, v in feedback_lst) + else: + return format_feedback('errors', response_items['feedback']) + + + 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': response_items['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']: + 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) + + # 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']) / 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 @@ -1832,4 +2176,5 @@ __all__ = [CodeResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, - JavascriptResponse] + JavascriptResponse, + OpenEndedResponse] diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html new file mode 100644 index 0000000000..65fc7fb9bb --- /dev/null +++ b/common/lib/capa/capa/templates/openendedinput.html @@ -0,0 +1,32 @@ +
    + + +
    + % 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} +
    +
    diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 10e984611b..0df58c216f 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -65,3 +65,25 @@ def is_file(file_to_test): Duck typing to check if 'file_to_test' is a File object ''' return all(hasattr(file_to_test, method) for method in ['read', 'name']) + + +def find_with_default(node, path, default): + """ + Look for a child of node using , and return its text if found. + Otherwise returns default. + + Arguments: + node: lxml node + path: xpath search expression + default: value to return if nothing found + + Returns: + node.find(path).text if the find succeeds, default otherwise. + + """ + v = node.find(path) + if v is not None: + return v.text + else: + return default + diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 0214488cce..798867955b 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -49,6 +49,7 @@ def parse_xreply(xreply): return_code = xreply['return_code'] content = xreply['content'] + return (return_code, content) @@ -80,7 +81,11 @@ class XQueueInterface(object): # Log in, then try again if error and (msg == 'login_required'): - self._login() + (error, content) = self._login() + if error != 0: + # when the login fails + log.debug("Failed to login to queue: %s", content) + return (error, content) if files_to_upload is not None: # Need to rewind file pointers for f in files_to_upload: diff --git a/common/lib/xmodule/.coveragerc b/common/lib/xmodule/.coveragerc index 310c8e778b..baadd30829 100644 --- a/common/lib/xmodule/.coveragerc +++ b/common/lib/xmodule/.coveragerc @@ -7,6 +7,7 @@ source = common/lib/xmodule ignore_errors = True [html] +title = XModule Python Test Coverage Report directory = reports/common/lib/xmodule/cover [xml] diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9922b1b8a0..0b0a035c27 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -145,6 +145,11 @@ class CapaModule(XModule): else: self.seed = None + # Need the problem location in openendedresponse to send out. Adding + # it to the system here seems like the least clunky way to get it + # there. + self.system.set('location', self.location.url()) + try: # TODO (vshnayder): move as much as possible of this work and error # checking to descriptor load time diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index a8f95c56bb..f3e81f1d84 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -366,7 +366,20 @@ class CourseDescriptor(SequenceDescriptor): @property def start_date_text(self): - displayed_start = self._try_parse_time('advertised_start') or self.start + parsed_advertised_start = self._try_parse_time('advertised_start') + + # If the advertised start isn't a real date string, we assume it's free + # form text... + if parsed_advertised_start is None and \ + ('advertised_start' in self.metadata): + return self.metadata['advertised_start'] + + displayed_start = parsed_advertised_start or self.start + + # If we have neither an advertised start or a real start, just return TBD + if not displayed_start: + return "TBD" + return time.strftime("%b %d, %Y", displayed_start) @property diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index fd67a3804e..b25ab3d3a2 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -121,16 +121,6 @@ section.problem { } } - &.processing { - p.status { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - width: 20px; - text-indent: -9999px; - } - } - &.correct, &.ui-icon-check { p.status { @include inline-block(); @@ -250,6 +240,13 @@ section.problem { } } + .reload + { + float:right; + margin: 10px; + } + + .grader-status { padding: 9px; background: #F6F6F6; @@ -266,6 +263,13 @@ section.problem { margin: -7px 7px 0 0; } + .grading { + background: url('../images/info-icon.png') left center no-repeat; + padding-left: 25px; + text-indent: 0px; + margin: 0px 7px 0 0; + } + p { line-height: 20px; text-transform: capitalize; @@ -685,6 +689,21 @@ section.problem { color: #B00; } } + + .markup-text{ + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + bs { + color: #BB0000; + } + + bg { + color: #BDA046; + } + } } } } diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js index b033dbaf46..bebe6b1854 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js +++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js @@ -1953,7 +1953,7 @@ cktsim = (function() { var module = { 'Circuit': Circuit, 'parse_number': parse_number, - 'parse_source': parse_source, + 'parse_source': parse_source } return module; }()); @@ -2068,7 +2068,7 @@ schematic = (function() { 'n': [NFet, 'NFet'], 'p': [PFet, 'PFet'], 's': [Probe, 'Voltage Probe'], - 'a': [Ammeter, 'Current Probe'], + 'a': [Ammeter, 'Current Probe'] }; // global clipboard @@ -5502,7 +5502,7 @@ schematic = (function() { 'magenta' : 'rgb(255,64,255)', 'yellow': 'rgb(255,255,64)', 'black': 'rgb(0,0,0)', - 'x-axis': undefined, + 'x-axis': undefined }; function Probe(x,y,rotation,color,offset) { @@ -6100,7 +6100,7 @@ schematic = (function() { 'Amplitude', 'Frequency (Hz)', 'Delay until sin starts (secs)', - 'Phase offset (degrees)'], + 'Phase offset (degrees)'] } // build property editor div @@ -6300,7 +6300,7 @@ schematic = (function() { var module = { 'Schematic': Schematic, - 'component_slider': component_slider, + 'component_slider': component_slider } return module; }()); diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index bb87679d37..1876330340 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -2,6 +2,8 @@ class @Video constructor: (element) -> @el = $(element).find('.video') @id = @el.attr('id').replace(/video_/, '') + @start = @el.data('start') + @end = @el.data('end') @caption_data_dir = @el.data('caption-data-dir') @caption_asset_path = @el.data('caption-asset-path') @show_captions = @el.data('show-captions') == "true" diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index b7c5bd8a89..93f90d9248 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -36,14 +36,21 @@ class @VideoPlayer extends Subview @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() @progressSlider = new VideoProgressSlider el: @$('.slider') + @playerVars = + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + modestbranding: 1 + if @video.start + @playerVars.start = @video.start + if @video.end + # work in AS3, not HMLT5. but iframe use AS3 + @playerVars.end = @video.end + @player = new YT.Player @video.id, - playerVars: - controls: 0 - wmode: 'transparent' - rel: 0 - showinfo: 0 - enablejsapi: 1 - modestbranding: 1 + playerVars: @playerVars videoId: @video.youtubeId() events: onReady: @onReady diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 55236b4f8e..e7f080dc8f 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -352,6 +352,12 @@ class ModuleStore(object): course_filter = Location("i4x", category="course") return self.get_items(course_filter) + def get_course(self, course_id): + ''' + Look for a specific course id. Returns the course descriptor, or None if not found. + ''' + raise NotImplementedError + def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed for path_to_location(). @@ -413,3 +419,10 @@ class ModuleStoreBase(ModuleStore): errorlog = self._get_errorlog(location) return errorlog.errors + + def get_course(self, course_id): + """Default impl--linear search through course list""" + for c in self.get_courses(): + if c.id == course_id: + return c + return None diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 8498a210cd..eb8a275d35 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -373,6 +373,14 @@ class SelfAssessmentModule(XModule): def save_answer(self, get): """ After the answer is submitted, show the rubric. + + Args: + get: the GET dictionary passed to the ajax request. Should contain + a key 'student_answer' + + Returns: + Dictionary with keys 'success' and either 'error' (if not success), + or 'rubric_html' (if success). """ # Check to see if attempts are less than max if self.attempts > self.max_attempts: diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index a7269a0f55..6dd92cc8fa 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -10,7 +10,7 @@ from xmodule.progress import Progress from xmodule.exceptions import NotFoundError from pkg_resources import resource_string -log = logging.getLogger("mitx.common.lib.seq_module") +log = logging.getLogger(__name__) # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 0593b7f00e..a38250dfb0 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -10,6 +10,9 @@ from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent +import datetime +import time + log = logging.getLogger(__name__) @@ -36,6 +39,7 @@ class VideoModule(XModule): self.show_captions = xmltree.get('show_captions', 'true') self.source = self._get_source(xmltree) self.track = self._get_track(xmltree) + self.start_time, self.end_time = self._get_timeframe(xmltree) if instance_state is not None: state = json.loads(instance_state) @@ -45,11 +49,11 @@ class VideoModule(XModule): def _get_source(self, xmltree): # find the first valid source return self._get_first_external(xmltree, 'source') - + def _get_track(self, xmltree): # find the first valid track return self._get_first_external(xmltree, 'track') - + def _get_first_external(self, xmltree, tag): """ Will return the first valid element @@ -64,6 +68,23 @@ class VideoModule(XModule): break return result + def _get_timeframe(self, xmltree): + """ Converts 'from' and 'to' parameters in video tag to seconds. + If there are no parameters, returns empty string. """ + + def parse_time(s): + """Converts s in '12:34:45' format to seconds. If s is + None, returns empty string""" + if s is None: + return '' + else: + x = time.strptime(s, '%H:%M:%S') + return datetime.timedelta(hours=x.tm_hour, + minutes=x.tm_min, + seconds=x.tm_sec).total_seconds() + + return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) + def handle_ajax(self, dispatch, get): ''' Handle ajax calls to this video. @@ -108,12 +129,14 @@ class VideoModule(XModule): 'id': self.location.html_id(), 'position': self.position, 'source': self.source, - 'track' : self.track, + 'track': self.track, 'display_name': self.display_name, # TODO (cpennington): This won't work when we move to data that isn't on the filesystem 'data_dir': self.metadata['data_dir'], 'caption_asset_path': caption_asset_path, - 'show_captions': self.show_captions + 'show_captions': self.show_captions, + 'start': self.start_time, + 'end': self.end_time }) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 88f4c3c48a..f213ba19ec 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -829,7 +829,8 @@ class ModuleSystem(object): debug=False, xqueue=None, node_path="", - anonymous_student_id=''): + anonymous_student_id='', + course_id=None): ''' Create a closure around the system environment. @@ -864,6 +865,8 @@ class ModuleSystem(object): ajax results. anonymous_student_id - Used for tracking modules with student id + + course_id - the course_id containing this module ''' self.ajax_url = ajax_url self.xqueue = xqueue @@ -876,6 +879,7 @@ class ModuleSystem(object): self.replace_urls = replace_urls self.node_path = node_path self.anonymous_student_id = anonymous_student_id + self.course_id = course_id self.user_is_staff = user is not None and user.is_staff def get(self, attr): diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee index a032c0248f..6b2714dc54 100644 --- a/common/static/coffee/src/discussion/utils.coffee +++ b/common/static/coffee/src/discussion/utils.coffee @@ -249,7 +249,10 @@ class @DiscussionUtil $3 else if RE_DISPLAYMATH.test(text) text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) -> - processedText += $1 + processor("$$" + $2 + "$$", 'display') + #processedText += $1 + processor("$$" + $2 + "$$", 'display') + #bug fix, ordering is off + processedText = processor("$$" + $2 + "$$", 'display') + processedText + processedText = $1 + processedText $3 else processedText += text diff --git a/common/static/sass/bourbon/css3/_box-sizing.scss b/common/static/sass/bourbon/css3/_box-sizing.scss index 3f3f7cca9a..e73e0fbd1c 100644 --- a/common/static/sass/bourbon/css3/_box-sizing.scss +++ b/common/static/sass/bourbon/css3/_box-sizing.scss @@ -2,5 +2,5 @@ // content-box | border-box | inherit -webkit-box-sizing: $box; -moz-box-sizing: $box; - box-sizing: $box; + box-sizing: $box; *behavior: url(/static/scripts/boxsizing.htc) } diff --git a/create-dev-env.sh b/create-dev-env.sh index 12ee163043..f0ebca3ff7 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -90,18 +90,18 @@ clone_repos() { fi } +### START + PROG=${0##*/} BASE="$HOME/mitx_all" PYTHON_DIR="$BASE/python" RUBY_DIR="$BASE/ruby" RUBY_VER="1.9.3" -NUMPY_VER="1.6.2" -SCIPY_VER="0.10.1" -BREW_FILE="$BASE/mitx/brew-formulas.txt" -APT_REPOS_FILE="$BASE/mitx/apt-repos.txt" -APT_PKGS_FILE="$BASE/mitx/apt-packages.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" + +# Read arguments + if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" usage @@ -163,18 +163,14 @@ info output "Press return to begin or control-C to abort" read dummy -# log all stdout and stderr + +# Log all stdout and stderr + exec > >(tee $LOG) exec 2>&1 -if ! grep -q "export rvm_path=$RUBY_DIR" ~/.rvmrc; then - if [[ -f $HOME/.rvmrc ]]; then - output "Copying existing .rvmrc to .rvmrc.bak" - cp $HOME/.rvmrc $HOME/.rvmrc.bak - fi - output "Creating $HOME/.rvmrc so rvm uses $RUBY_DIR" - echo "export rvm_path=$RUBY_DIR" > $HOME/.rvmrc -fi + +# Install basic system requirements mkdir -p $BASE case `uname -s` in @@ -187,19 +183,7 @@ case `uname -s` in distro=`lsb_release -cs` case $distro in maya|lisa|natty|oneiric|precise|quantal) - output "Installing ubuntu requirements" - - # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation - export DEBIAN_FRONTEND=noninteractive - - # add repositories - cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y - sudo apt-get -y update - - # install packages listed in APT_PKGS_FILE - cat $APT_PKGS_FILE | xargs sudo apt-get -y install - - clone_repos + sudo apt-get install git ;; *) error "Unsupported distribution - $distro" @@ -207,8 +191,8 @@ case `uname -s` in ;; esac ;; - Darwin) + Darwin) if [[ ! -w /usr/local ]]; then cat</dev/null || { - output "Installing $pkg" - brew install $pkg - } - done - - # paths where brew likes to install python scripts - PATH=/usr/local/share/python:/usr/local/bin:$PATH - - command -v pip &>/dev/null || { - output "Installing pip" - easy_install pip - } - - if ! grep -Eq ^1.7 <(virtualenv --version 2>/dev/null); then - output "Installing virtualenv >1.7" - pip install 'virtualenv>1.7' virtualenvwrapper - fi - - command -v coffee &>/dev/null || { - output "Installing coffee script" - curl --insecure https://npmjs.org/install.sh | sh - npm install -g coffee-script - } ;; *) error "Unsupported platform" @@ -275,19 +226,54 @@ EO ;; esac + +# Clone MITx repositories + +clone_repos + + +# Install system-level dependencies + +bash $BASE/mitx/install-system-req.sh + + +# Install Ruby RVM + output "Installing rvm and ruby" + +if ! grep -q "export rvm_path=$RUBY_DIR" ~/.rvmrc; then + if [[ -f $HOME/.rvmrc ]]; then + output "Copying existing .rvmrc to .rvmrc.bak" + cp $HOME/.rvmrc $HOME/.rvmrc.bak + fi + output "Creating $HOME/.rvmrc so rvm uses $RUBY_DIR" + echo "export rvm_path=$RUBY_DIR" > $HOME/.rvmrc +fi + curl -sL get.rvm.io | bash -s -- --version 1.15.7 source $RUBY_DIR/scripts/rvm -# skip the intro LESS="-E" rvm install $RUBY_VER --with-readline + output "Installing gem bundler" gem install bundler + output "Installing ruby packages" # hack :( cd $BASE/mitx || true bundle install -cd $BASE + +# Install Python virtualenv + +output "Installing python virtualenv" + +case `uname -s` in + Darwin) + # Add brew's path + PATH=/usr/local/share/python:/usr/local/bin:$PATH + ;; +esac + if [[ $systempkgs ]]; then virtualenv --system-site-packages "$PYTHON_DIR" else @@ -296,9 +282,14 @@ else virtualenv "$PYTHON_DIR" fi -# change to mitx python virtualenv +# activate mitx python virtualenv source $PYTHON_DIR/bin/activate +# compile numpy and scipy if requested + +NUMPY_VER="1.6.2" +SCIPY_VER="0.10.1" + if [[ -n $compile ]]; then output "Downloading numpy and scipy" curl -sL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz @@ -330,18 +321,25 @@ case `uname -s` in esac output "Installing MITx pre-requirements" -pip install -r mitx/pre-requirements.txt -# Need to be in the mitx dir to get the paths to local modules right +pip install -r $BASE/mitx/pre-requirements.txt + output "Installing MITx requirements" -cd mitx +# Need to be in the mitx dir to get the paths to local modules right +cd $BASE/mitx pip install -r requirements.txt mkdir "$BASE/log" || true mkdir "$BASE/db" || true + +# Configure Git + output "Fixing your git default settings" git config --global push.default current + +### DONE + cat</dev/null || { + error "Please install lsb-release." + exit 1 + } + + distro=`lsb_release -cs` + case $distro in + maya|lisa|natty|oneiric|precise|quantal) + output "Installing Ubuntu requirements" + + # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation + export DEBIAN_FRONTEND=noninteractive + + # add repositories + cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y + sudo apt-get -y update + + # install packages listed in APT_PKGS_FILE + cat $APT_PKGS_FILE | xargs sudo apt-get -y install + ;; + *) + error "Unsupported distribution - $distro" + exit 1 + ;; + esac + ;; + Darwin) + + if [[ ! -w /usr/local ]]; then + cat</dev/null || { + output "Installing $pkg" + brew install $pkg + } + done + + # paths where brew likes to install python scripts + PATH=/usr/local/share/python:/usr/local/bin:$PATH + + command -v pip &>/dev/null || { + output "Installing pip" + easy_install pip + } + + if ! grep -Eq ^1.7 <(virtualenv --version 2>/dev/null); then + output "Installing virtualenv >1.7" + pip install 'virtualenv>1.7' virtualenvwrapper + fi + + command -v coffee &>/dev/null || { + output "Installing coffee script" + curl --insecure https://npmjs.org/install.sh | sh + npm install -g coffee-script + } + ;; + *) + error "Unsupported platform" + exit 1 + ;; +esac diff --git a/lms/.coveragerc b/lms/.coveragerc index acac3ed4f2..7e18a37492 100644 --- a/lms/.coveragerc +++ b/lms/.coveragerc @@ -2,11 +2,13 @@ [run] data_file = reports/lms/.coverage source = lms +omit = lms/envs/* [report] ignore_errors = True [html] +title = LMS Python Test Coverage Report directory = reports/lms/cover [xml] diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 0bd2311021..6dc38ef016 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -43,7 +43,8 @@ def has_access(user, obj, action, course_context=None): user: a Django user object. May be anonymous. - obj: The object to check access for. For now, a module or descriptor. + obj: The object to check access for. A module, descriptor, location, or + certain special strings (e.g. 'global') action: A string specifying the action that the client is trying to perform. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 6de309076d..72de6a0dad 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -236,11 +236,51 @@ def get_courses_by_university(user, domain=None): ''' # TODO: Clean up how 'error' is done. # filter out any courses that errored. - visible_courses = branding.get_visible_courses(domain) + visible_courses = get_courses(user, domain) universities = defaultdict(list) for course in visible_courses: - if not has_access(user, course, 'see_exists'): - continue universities[course.org].append(course) + return universities + + +def get_courses(user, domain=None): + ''' + Returns a list of courses available, sorted by course.number + ''' + courses = branding.get_visible_courses(domain) + courses = [c for c in courses if has_access(user, c, 'see_exists')] + + # Add metadata about the start day and if the course is new + for course in courses: + days_to_start = _get_course_days_to_start(course) + + metadata = course.metadata + metadata['days_to_start'] = days_to_start + metadata['is_new'] = course.metadata.get('is_new', days_to_start > 1) + + courses = sorted(courses, key=lambda course:course.number) + return courses + + +def _get_course_days_to_start(course): + from datetime import datetime as dt + from time import mktime, gmtime + + convert_to_datetime = lambda ts: dt.fromtimestamp(mktime(ts)) + + start_date = convert_to_datetime(course.start) + + # If the course has a valid advertised date, use that instead + advertised_start = course.metadata.get('advertised_start', None) + if advertised_start: + try: + start_date = dt.strptime(advertised_start, "%Y-%m-%dT%H:%M") + except ValueError: + pass # Invalid date, keep using course.start + + now = convert_to_datetime(gmtime()) + days_to_start = (start_date - now).days + + return days_to_start diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 9343301fb7..ecfe76610e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,4 +1,3 @@ -import hashlib import json import logging import pyparsing @@ -20,6 +19,7 @@ from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from static_replace import replace_urls +from student.models import unique_id_for_user from xmodule.errortracker import exc_info_to_str from xmodule.exceptions import NotFoundError from xmodule.modulestore import Location @@ -157,12 +157,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi if not has_access(user, descriptor, 'load', course_id): return None - # Anonymized student identifier - h = hashlib.md5() - h.update(settings.SECRET_KEY) - h.update(str(user.id)) - anonymous_student_id = h.hexdigest() - # Only check the cache if this module can possibly have state instance_module = None shared_module = None @@ -235,7 +229,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi # by the replace_static_urls code below replace_urls=replace_urls, node_path=settings.NODE_PATH, - anonymous_student_id=anonymous_student_id, + anonymous_student_id=unique_id_for_user(user), + course_id=course_id, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index e926a0b114..acba61c90b 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -45,7 +45,7 @@ CourseTab = namedtuple('CourseTab', 'name link is_active') # wrong. (e.g. "is there a 'name' field?). Validators can assume # that the type field is valid. # -# - a function that takes a config, a user, and a course, and active_page and +# - a function that takes a config, a user, and a course, an active_page and # return a list of CourseTabs. (e.g. "return a CourseTab with specified # name, link to courseware, and is_active=True/False"). The function can # assume that it is only called with configs of the appropriate type that @@ -106,6 +106,14 @@ def _textbooks(tab, user, course, active_page): for index, textbook in enumerate(course.textbooks)] return [] + +def _staff_grading(tab, user, course, active_page): + if has_access(user, course, 'staff'): + link = reverse('staff_grading', args=[course.id]) + return [CourseTab('Staff grading', link, active_page == "staff_grading")] + return [] + + #### Validators @@ -141,6 +149,7 @@ 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), } diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index f6f6f93c04..6c85a0e268 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -215,13 +215,27 @@ class PageLoader(ActivateLoginTestCase): def check_for_get_code(self, code, url): """ - Check that we got the expected code. Hacks around our broken 404 - handling. + Check that we got the expected code when accessing url via GET. + Returns the response. """ resp = self.client.get(url) self.assertEqual(resp.status_code, code, "got code {0} for url '{1}'. Expected code {2}" .format(resp.status_code, url, code)) + return resp + + + def check_for_post_code(self, code, url, data={}): + """ + Check that we got the expected code when accessing url via POST. + Returns the response. + """ + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, code, + "got code {0} for url '{1}'. Expected code {2}" + .format(resp.status_code, url, code)) + return resp + def check_pages_load(self, module_store): @@ -345,14 +359,10 @@ class TestNavigation(PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(course_id): - """Assumes the course is present""" - return [c for c in courses if c.id==course_id][0] - - self.full = find_course("edX/full/6.002_Spring_2012") - self.toy = find_course("edX/toy/2012_Fall") + # Assume courses are there + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -403,14 +413,9 @@ class TestViewAuth(PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(course_id): - """Assumes the course is present""" - return [c for c in courses if c.id==course_id][0] - - self.full = find_course("edX/full/6.002_Spring_2012") - self.toy = find_course("edX/toy/2012_Fall") + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -688,46 +693,46 @@ class TestCourseGrader(PageLoader): return [c for c in courses if c.id==course_id][0] self.graded_course = find_course("edX/graded/2012_Fall") - + # create a test student self.student = 'view@test.com' self.password = 'foo' self.create_account('u1', self.student, self.password) self.activate_user(self.student) self.enroll(self.graded_course) - + self.student_user = user(self.student) - + self.factory = RequestFactory() - + def get_grade_summary(self): student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( self.graded_course.id, self.student_user, self.graded_course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.graded_course.id})) - - return grades.grade(self.student_user, fake_request, - self.graded_course, student_module_cache) - - def get_homework_scores(self): - return self.get_grade_summary()['totaled_scores']['Homework'] - - def get_progress_summary(self): - student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - self.graded_course.id, self.student_user, self.graded_course) - + fake_request = self.factory.get(reverse('progress', kwargs={'course_id': self.graded_course.id})) - progress_summary = grades.progress_summary(self.student_user, fake_request, + return grades.grade(self.student_user, fake_request, + self.graded_course, student_module_cache) + + def get_homework_scores(self): + return self.get_grade_summary()['totaled_scores']['Homework'] + + def get_progress_summary(self): + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( + self.graded_course.id, self.student_user, self.graded_course) + + fake_request = self.factory.get(reverse('progress', + kwargs={'course_id': self.graded_course.id})) + + progress_summary = grades.progress_summary(self.student_user, fake_request, self.graded_course, student_module_cache) return progress_summary - + def check_grade_percent(self, percent): grade_summary = self.get_grade_summary() - self.assertEqual(percent, grade_summary['percent']) - + self.assertEqual(grade_summary['percent'], percent) + def submit_question_answer(self, problem_url_name, responses): """ The field names of a problem are hard to determine. This method only works @@ -737,96 +742,96 @@ class TestCourseGrader(PageLoader): input_i4x-edX-graded-problem-H1P3_2_2 """ problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name) - - modx_url = reverse('modx_dispatch', + + modx_url = reverse('modx_dispatch', kwargs={ 'course_id' : self.graded_course.id, 'location' : problem_location, 'dispatch' : 'problem_check', } ) - + resp = self.client.post(modx_url, { 'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0], 'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1], }) print "modx_url" , modx_url, "responses" , responses print "resp" , resp - + return resp - + def problem_location(self, problem_url_name): return "i4x://edX/graded/problem/{0}".format(problem_url_name) - + def reset_question_answer(self, problem_url_name): problem_location = self.problem_location(problem_url_name) - - modx_url = reverse('modx_dispatch', + + modx_url = reverse('modx_dispatch', kwargs={ 'course_id' : self.graded_course.id, 'location' : problem_location, 'dispatch' : 'problem_reset', } ) - + resp = self.client.post(modx_url) - return resp - + return resp + def test_get_graded(self): #### Check that the grader shows we have 0% in the course self.check_grade_percent(0) - + #### Submit the answers to a few problems as ajax calls def earned_hw_scores(): """Global scores, each Score is a Problem Set""" return [s.earned for s in self.get_homework_scores()] - + def score_for_hw(hw_url_name): hw_section = [section for section in self.get_progress_summary()[0]['sections'] if section.get('url_name') == hw_url_name][0] return [s.earned for s in hw_section['scores']] - + # Only get half of the first problem correct self.submit_question_answer('H1P1', ['Correct', 'Incorrect']) self.check_grade_percent(0.06) self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0]) - + # Get both parts of the first problem correct self.reset_question_answer('H1P1') self.submit_question_answer('H1P1', ['Correct', 'Correct']) self.check_grade_percent(0.13) self.assertEqual(earned_hw_scores(), [2.0, 0, 0]) self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0]) - + # This problem is shown in an ABTest self.submit_question_answer('H1P2', ['Correct', 'Correct']) self.check_grade_percent(0.25) self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0]) - self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - + self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) + # This problem is hidden in an ABTest. Getting it correct doesn't change total grade self.submit_question_answer('H1P3', ['Correct', 'Correct']) self.check_grade_percent(0.25) self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - + # On the second homework, we only answer half of the questions. # Then it will be dropped when homework three becomes the higher percent # This problem is also weighted to be 4 points (instead of default of 2) - # If the problem was unweighted the percent would have been 0.38 so we + # If the problem was unweighted the percent would have been 0.38 so we # know it works. self.submit_question_answer('H2P1', ['Correct', 'Correct']) self.check_grade_percent(0.42) - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0]) + # Third homework self.submit_question_answer('H3P1', ['Correct', 'Correct']) self.check_grade_percent(0.42) # Score didn't change - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) + self.submit_question_answer('H3P2', ['Correct', 'Correct']) self.check_grade_percent(0.5) # Now homework2 dropped. Score changes - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) + # Now we answer the final question (worth half of the grade) self.submit_question_answer('FinalQuestion', ['Correct', 'Correct']) self.check_grade_percent(1.0) # Hooray! We got 100% diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 4472eee7fa..2d271782c2 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -17,7 +17,7 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access -from courseware.courses import (get_course_with_access, get_courses_by_university) +from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university) import courseware.tabs as tabs from courseware.models import StudentModuleCache from module_render import toc_for_course, get_module, get_instance_module @@ -61,16 +61,19 @@ def user_groups(user): return group_names - @ensure_csrf_cookie @cache_if_anonymous def courses(request): ''' Render "find courses" page. The course selection work is done in courseware.courses. ''' - universities = get_courses_by_university(request.user, - domain=request.META.get('HTTP_HOST')) - return render_to_response("courseware/courses.html", {'universities': universities}) + 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.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) + + return render_to_response("courseware/courses.html", {'courses': courses}) def render_accordion(request, course, chapter, section): @@ -317,7 +320,7 @@ def jump_to(request, course_id, location): except NoPathToItem: raise Http404("This location is not in any class: {0}".format(location)) - # choose the appropriate view (and provide the necessary args) based on the + # choose the appropriate view (and provide the necessary args) based on the # args provided by the redirect. # Rely on index to do all error handling and access control. if chapter is None: @@ -328,7 +331,7 @@ def jump_to(request, course_id, location): return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section) else: return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position) - + @ensure_csrf_cookie def course_info(request, course_id): """ @@ -435,6 +438,11 @@ 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.metadata['days_to_start'] + courses = sorted(courses, key=key, reverse=True) + context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 628ac21a4a..6d120be79d 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -2,6 +2,9 @@ import logging from django.db import models from django.contrib.auth.models import User +from student.models import CourseEnrollment +from django.db.models.signals import post_save +from django.dispatch import receiver from courseware.courses import get_course_by_id @@ -10,6 +13,18 @@ FORUM_ROLE_MODERATOR = 'Moderator' FORUM_ROLE_COMMUNITY_TA = 'Community TA' FORUM_ROLE_STUDENT = 'Student' + +@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) + + class Role(models.Model): name = models.CharField(max_length=30, null=False, blank=False) users = models.ManyToManyField(User, related_name="roles") diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index fbb87a1584..71fc38c0e1 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -17,6 +17,7 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.search import path_to_location +log = logging.getLogger(__name__) # TODO these should be cached via django's caching rather than in-memory globals _FULLMODULES = None @@ -141,6 +142,15 @@ def initialize_discussion_info(course): for location, module in all_modules.items(): if location.category == 'discussion': + skip_module = False + for key in ('id', 'discussion_category', 'for'): + if key not in module.metadata: + log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) + skip_module = True + + if skip_module: + continue + id = module.metadata['id'] category = module.metadata['discussion_category'] title = module.metadata['for'] @@ -245,7 +255,7 @@ class QueryCountDebugMiddleware(object): query_time = query.get('duration', 0) / 1000 total_time += float(query_time) - logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time)) + log.info('%s queries run, total %s seconds' % (len(connection.queries), total_time)) return response def get_ability(course_id, content, user): @@ -317,7 +327,7 @@ def extend_content(content): user = User.objects.get(pk=content['user_id']) roles = dict(('name', role.name.lower()) for role in user.roles.filter(course_id=content['course_id'])) except user.DoesNotExist: - logging.error('User ID {0} in comment content {1} but not in our DB.'.format(content.get('user_id'), content.get('id'))) + log.error('User ID {0} in comment content {1} but not in our DB.'.format(content.get('user_id'), content.get('id'))) content_info = { 'displayed_title': content.get('highlighted_title') or content.get('title', ''), diff --git a/lms/djangoapps/instructor/grading.py b/lms/djangoapps/instructor/grading.py new file mode 100644 index 0000000000..7a48b25a49 --- /dev/null +++ b/lms/djangoapps/instructor/grading.py @@ -0,0 +1,25 @@ +""" +LMS part of instructor grading: + +- views + ajax handling +- calls the instructor grading service +""" + +import json +import logging + +log = logging.getLogger(__name__) + + +class StaffGrading(object): + """ + Wrap up functionality for staff grading of submissions--interface exposes get_html, ajax views. + """ + def __init__(self, course): + self.course = course + + def get_html(self): + return "Instructor grading!" + # context = {} + # return render_to_string('courseware/instructor_grading_view.html', context) + diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py new file mode 100644 index 0000000000..ea8f0de074 --- /dev/null +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -0,0 +1,390 @@ +""" +This module provides views that proxy to the staff grading backend service. +""" + +import json +import logging +import requests +from requests.exceptions import RequestException, ConnectionError, HTTPError +import sys + +from django.conf import settings +from django.http import HttpResponse, Http404 + +from courseware.access import has_access +from util.json_request import expect_json +from xmodule.course_module import CourseDescriptor + +log = logging.getLogger(__name__) + + +class GradingServiceError(Exception): + pass + + +class MockStaffGradingService(object): + """ + A simple mockup of a staff grading service, testing. + """ + def __init__(self): + self.cnt = 0 + + def get_next(self,course_id, location, grader_id): + self.cnt += 1 + return json.dumps({'success': True, + 'submission_id': self.cnt, + 'submission': 'Test submission {cnt}'.format(cnt=self.cnt), + 'num_graded': 3, + 'min_for_ml': 5, + 'num_pending': 4, + 'prompt': 'This is a fake prompt', + 'ml_error_info': 'ML info', + 'max_score': 2 + self.cnt % 3, + 'rubric': 'A rubric'}) + + def get_problem_list(self, course_id, grader_id): + self.cnt += 1 + return json.dumps({'success': True, + 'problem_list': [ + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', \ + 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'min_for_ml': 10}), + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', \ + 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'min_for_ml': 10}) + ]}) + + + def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): + return self.get_next(course_id, 'fake location', grader_id) + + +class StaffGradingService(object): + """ + Interface to staff grading backend. + """ + def __init__(self, config): + self.username = config['username'] + self.password = config['password'] + self.url = config['url'] + + self.login_url = self.url + '/login/' + self.get_next_url = self.url + '/get_next_submission/' + self.save_grade_url = self.url + '/save_grade/' + self.get_problem_list_url = self.url + '/get_problem_list/' + + self.session = requests.session() + + + def _login(self): + """ + Log into the staff grading service. + + Raises requests.exceptions.HTTPError if something goes wrong. + + Returns the decoded json dict of the response. + """ + response = self.session.post(self.login_url, + {'username': self.username, + 'password': self.password,}) + + response.raise_for_status() + + return response.json + + + def _try_with_login(self, operation): + """ + Call operation(), which should return a requests response object. If + the request fails with a 'login_required' error, call _login() and try + the operation again. + + Returns the result of operation(). Does not catch exceptions. + """ + response = operation() + if (response.json + and response.json.get('success') == False + and response.json.get('error') == 'login_required'): + # apparrently we aren't logged in. Try to fix that. + r = self._login() + if r and not r.get('success'): + log.warning("Couldn't log into staff_grading backend. Response: %s", + r) + # try again + response = operation() + response.raise_for_status() + + return response + + def get_problem_list(self, course_id, grader_id): + """ + Get the list of problems for a given course. + + Args: + course_id: course id that we want the problems of + grader_id: who is grading this? The anonymous user_id of the grader. + + Returns: + json string with the response from the service. (Deliberately not + writing out the fields here--see the docs on the staff_grading view + in the grading_controller repo) + + Raises: + GradingServiceError: something went wrong with the connection. + """ + op = lambda: self.session.get(self.get_problem_list_url, + allow_redirects = False, + params={'course_id': course_id, + 'grader_id': grader_id}) + try: + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + + + def get_next(self, course_id, location, grader_id): + """ + Get the next thing to grade. + + Args: + course_id: the course that this problem belongs to + location: location of the problem that we are grading and would like the + next submission for + grader_id: who is grading this? The anonymous user_id of the grader. + + Returns: + json string with the response from the service. (Deliberately not + writing out the fields here--see the docs on the staff_grading view + in the grading_controller repo) + + Raises: + GradingServiceError: something went wrong with the connection. + """ + op = lambda: self.session.get(self.get_next_url, + allow_redirects=False, + params={'location': location, + 'grader_id': grader_id}) + try: + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + + + def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): + """ + Save a score and feedback for a submission. + + Returns: + json dict with keys + 'success': bool + 'error': error msg, if something went wrong. + + Raises: + GradingServiceError if there's a problem connecting. + """ + try: + data = {'course_id': course_id, + 'submission_id': submission_id, + 'score': score, + 'feedback': feedback, + 'grader_id': grader_id, + 'skipped': skipped} + + op = lambda: self.session.post(self.save_grade_url, data=data, + allow_redirects=False) + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + +# don't initialize until grading_service() is called--means that just +# importing this file doesn't create objects that may not have the right config +_service = None + +def grading_service(): + """ + Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True, + returns a mock one, otherwise a real one. + + Caches the result, so changing the setting after the first call to this + function will have no effect. + """ + global _service + if _service is not None: + return _service + + if settings.MOCK_STAFF_GRADING: + _service = MockStaffGradingService() + else: + _service = StaffGradingService(settings.STAFF_GRADING_INTERFACE) + + return _service + +def _err_response(msg): + """ + Return a HttpResponse with a json dump with success=False, and the given error message. + """ + return HttpResponse(json.dumps({'success': False, 'error': msg}), + mimetype="application/json") + + +def _check_access(user, course_id): + """ + Raise 404 if user doesn't have staff access to course_id + """ + course_location = CourseDescriptor.id_to_location(course_id) + if not has_access(user, course_location, 'staff'): + raise Http404 + + return + + +def get_next(request, course_id): + """ + Get the next thing to grade for course_id and with the location specified + in the . + + Returns a json dict with the following keys: + + 'success': bool + + 'submission_id': a unique identifier for the submission, to be passed back + with the grade. + + 'submission': the submission, rendered as read-only html for grading + + 'rubric': the rubric, also rendered as html. + + 'message': if there was no submission available, but nothing went wrong, + there will be a message field. + + 'error': if success is False, will have an error message with more info. + """ + _check_access(request.user, course_id) + + required = set(['location']) + if request.method != 'POST': + raise Http404 + actual = set(request.POST.keys()) + missing = required - actual + if len(missing) > 0: + return _err_response('Missing required keys {0}'.format( + ', '.join(missing))) + grader_id = request.user.id + p = request.POST + location = p['location'] + + return HttpResponse(_get_next(course_id, request.user.id, location), + mimetype="application/json") + + +def get_problem_list(request, course_id): + """ + Get all the problems for the given course id + + Returns a json dict with the following keys: + success: bool + + problem_list: a list containing json dicts with the following keys: + each dict represents a different problem in the course + + location: the location of the problem + + problem_name: the name of the problem + + num_graded: the number of responses that have been graded + + num_pending: the number of responses that are sitting in the queue + + min_for_ml: the number of responses that need to be graded before + the ml can be run + + """ + _check_access(request.user, course_id) + try: + response = grading_service().get_problem_list(course_id, request.user.id) + return HttpResponse(response, + mimetype="application/json") + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(grading_service().url)) + return HttpResponse(json.dumps({'success': False, + 'error': 'Could not connect to grading service'})) + + +def _get_next(course_id, grader_id, location): + """ + Implementation of get_next (also called from save_grade) -- returns a json string + """ + try: + return grading_service().get_next(course_id, location, grader_id) + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(grading_service().url)) + return json.dumps({'success': False, + 'error': 'Could not connect to grading service'}) + + +@expect_json +def save_grade(request, course_id): + """ + Save the grade and feedback for a submission, and, if all goes well, return + the next thing to grade. + + Expects the following POST parameters: + 'score': int + 'feedback': string + 'submission_id': int + + Returns the same thing as get_next, except that additional error messages + are possible if something goes wrong with saving the grade. + """ + _check_access(request.user, course_id) + + if request.method != 'POST': + raise Http404 + + required = set(['score', 'feedback', 'submission_id', 'location']) + actual = set(request.POST.keys()) + missing = required - actual + if len(missing) > 0: + return _err_response('Missing required keys {0}'.format( + ', '.join(missing))) + + grader_id = request.user.id + p = request.POST + + + location = p['location'] + skipped = 'skipped' in p + try: + result_json = grading_service().save_grade(course_id, + grader_id, + p['submission_id'], + p['score'], + p['feedback'], + skipped) + except GradingServiceError: + log.exception("Error saving grade") + return _err_response('Could not connect to grading service') + + try: + result = json.loads(result_json) + except ValueError: + log.exception("save_grade returned broken json: %s", result_json) + return _err_response('Grading service returned mal-formatted data.') + + if not result.get('success', False): + log.warning('Got success=False from grading service. Response: %s', result_json) + return _err_response('Grading service failed') + + # Ok, save_grade seemed to work. Get the next submission to grade. + return HttpResponse(_get_next(course_id, grader_id, location), + mimetype="application/json") + diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 532c0c3f68..865a97951e 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -8,15 +8,24 @@ Notes for running by hand: django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor """ +import courseware.tests.tests as ct + +import json + +from nose import SkipTest +from mock import patch, Mock + from override_settings import override_settings -from django.contrib.auth.models import \ - Group # Need access to internal func to put users in the right group +# Need access to internal func to put users in the right group +from django.contrib.auth.models import Group + from django.core.urlresolvers import reverse from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \ FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT from django_comment_client.utils import has_forum_access +from instructor import staff_grading_service from courseware.access import _course_staff_group_name import courseware.tests.tests as ct from xmodule.modulestore.django import modulestore @@ -31,14 +40,9 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(name): - """Assumes the course is present""" - return [c for c in courses if c.location.course==name][0] - - self.full = find_course("full") - self.toy = find_course("toy") + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -49,9 +53,12 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): self.activate_user(self.student) self.activate_user(self.instructor) - group_name = _course_staff_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(ct.user(self.instructor)) + + make_instructor(self.toy) self.logout() self.login(self.instructor, self.password) @@ -67,18 +74,21 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): self.assertEqual(response['Content-Type'],'text/csv',msg) - cdisp = response['Content-Disposition'].replace('TT_2012','2012') # jenkins course_id is TT_2012_Fall instead of 2012_Fall? - msg += "cdisp = '{0}'\n".format(cdisp) - self.assertEqual(cdisp,'attachment; filename=grades_edX/toy/2012_Fall.csv',msg) + cdisp = response['Content-Disposition'] + msg += "Content-Disposition = '%s'\n" % cdisp + self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg) body = response.content.replace('\r','') msg += "body = '{0}'\n".format(body) + # All the not-actually-in-the-course hw and labs come from the + # default grading policy string in graders.py expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0" ''' self.assertEqual(body, expected_body, msg) - + + FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ] FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'} FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'} @@ -89,22 +99,22 @@ def action_name(operation, rolename): else: return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) + +_mock_service = staff_grading_service.MockStaffGradingService() + @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) class TestInstructorDashboardForumAdmin(ct.PageLoader): ''' Check for change in forum admin role memberships ''' - + def setUp(self): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() - def find_course(name): - """Assumes the course is present""" - return [c for c in courses if c.location.course==name][0] - self.full = find_course("full") - self.toy = find_course("toy") + self.course_id = "edX/toy/2012_Fall" + self.toy = modulestore().get_course(self.course_id) # Create two accounts self.student = 'view@test.com' @@ -123,6 +133,8 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): self.login(self.instructor, self.password) self.enroll(self.toy) + + def initialize_roles(self, course_id): self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0] self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0] @@ -209,3 +221,96 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): added_roles.sort() roles = ', '.join(added_roles) self.assertTrue(response.content.find('{0}'.format(roles))>=0, 'not finding roles "{0}"'.format(roles)) + + +@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) +class TestStaffGradingService(ct.PageLoader): + ''' + Check that staff grading service proxy works. Basically just checking the + access control and error handling logic -- all the actual work is on the + backend. + ''' + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.student = 'view@test.com' + self.instructor = 'view2@test.com' + self.password = 'foo' + self.location = 'TestLocation' + self.create_account('u1', self.student, self.password) + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.student) + self.activate_user(self.instructor) + + self.course_id = "edX/toy/2012_Fall" + self.toy = modulestore().get_course(self.course_id) + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(ct.user(self.instructor)) + + make_instructor(self.toy) + + self.mock_service = staff_grading_service.grading_service() + + self.logout() + + def test_access(self): + """ + Make sure only staff have access. + """ + self.login(self.student, self.password) + + # both get and post should return 404 + for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): + url = reverse(view_name, kwargs={'course_id': self.course_id}) + self.check_for_get_code(404, url) + self.check_for_post_code(404, url) + + + def test_get_next(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) + data = {'location': self.location} + + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success']) + self.assertEquals(d['submission_id'], self.mock_service.cnt) + self.assertIsNotNone(d['submission']) + self.assertIsNotNone(d['num_graded']) + self.assertIsNotNone(d['min_for_ml']) + self.assertIsNotNone(d['num_pending']) + self.assertIsNotNone(d['prompt']) + self.assertIsNotNone(d['ml_error_info']) + self.assertIsNotNone(d['max_score']) + self.assertIsNotNone(d['rubric']) + + + def test_save_grade(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) + + data = {'score': '12', + 'feedback': 'great!', + 'submission_id': '123', + 'location': self.location} + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success'], str(d)) + self.assertEquals(d['submission_id'], self.mock_service.cnt) + + def test_get_problem_list(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) + data = {} + + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success'], str(d)) + self.assertIsNotNone(d['problem_list']) + + diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f985cc43a0..79cf0caaf3 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -12,6 +12,7 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response +from django.core.urlresolvers import reverse from courseware import grades from courseware.access import has_access, get_access_group_name @@ -27,7 +28,10 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -log = logging.getLogger("mitx.courseware") +from .grading import StaffGrading + + +log = logging.getLogger(__name__) template_imports = {'urllib': urllib} @@ -87,7 +91,7 @@ def instructor_dashboard(request, course_id): try: group = Group.objects.get(name=staffgrp) except Group.DoesNotExist: - group = Group(name=staffgrp) # create the group + group = Group(name=staffgrp) # create the group group.save() return group @@ -377,7 +381,7 @@ 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'] - if get_grades: + 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)) @@ -409,6 +413,29 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, return datatable + +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def staff_grading(request, course_id): + """ + Show the instructor grading interface. + """ + course = get_course_with_access(request.user, course_id, 'staff') + + grading = StaffGrading(course) + + ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + + return render_to_response('instructor/staff_grading.html', { + 'view_html': grading.get_html(), + 'course': course, + 'course_id': course_id, + 'ajax_url': ajax_url, + # Checked above + 'staff_access': True, }) + + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): """ diff --git a/lms/envs/aws.py b/lms/envs/aws.py index e6242ace56..83f6c83f15 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -83,5 +83,7 @@ XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] MODULESTORE = AUTH_TOKENS.get('MODULESTORE', MODULESTORE) CONTENTSTORE = AUTH_TOKENS.get('CONTENTSTORE', CONTENTSTORE) +STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE') + PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") diff --git a/lms/envs/common.py b/lms/envs/common.py index 85918c7c33..4118f3d744 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -185,6 +185,9 @@ DEBUG_TRACK_LOG = False MITX_ROOT_URL = '' +LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/accounts/login' +LOGIN_URL = MITX_ROOT_URL + '/accounts/login' + COURSE_NAME = "6.002_Spring_2012" COURSE_NUMBER = "6.002x" COURSE_TITLE = "Circuits and Electronics" @@ -321,6 +324,13 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False WIKI_LINK_LIVE_LOOKUPS = False WIKI_LINK_DEFAULT_LEVEL = 2 +################################# Staff grading config ##################### + +STAFF_GRADING_INTERFACE = None +# Used for testing, debugging +MOCK_STAFF_GRADING = False + + ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' @@ -387,6 +397,7 @@ courseware_js = ( ) main_vendor_js = [ + 'js/vendor/json2.js', 'js/vendor/jquery.min.js', 'js/vendor/jquery-ui.min.js', 'js/vendor/jquery.cookie.js', @@ -397,6 +408,8 @@ main_vendor_js = [ discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) +staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) + PIPELINE_CSS = { 'application': { 'source_filenames': ['sass/application.scss'], @@ -425,8 +438,9 @@ PIPELINE_JS = { 'source_filenames': sorted( set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - - set(courseware_js + discussion_js) + set(courseware_js + discussion_js + staff_grading_js) ) + [ + 'js/form.ext.js', 'js/my_courses_dropdown.js', 'js/toggle_login_modal.js', @@ -451,9 +465,12 @@ PIPELINE_JS = { 'source_filenames': discussion_js, 'output_filename': 'js/discussion.js' }, + 'staff_grading' : { + 'source_filenames': staff_grading_js, + 'output_filename': 'js/staff_grading.js' + } } - PIPELINE_DISABLE_WRAPPER = True # Compile all coffee files in course data directories if they are out of date. diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 5c230cdbfc..d67125f1ba 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -39,7 +39,7 @@ DATABASES = { } CACHES = { - # This is the cache used for most things. + # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -106,7 +106,13 @@ VIRTUAL_UNIVERSITIES = [] COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" +################################# Staff grading config ##################### +STAFF_GRADING_INTERFACE = { + 'url': 'http://127.0.0.1:3033/staff_grading', + 'username': 'lms', + 'password': 'abcd', + } ################################ LMS Migration ################################# MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index b15e3acb4c..7c6688b493 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -44,12 +44,6 @@ STATUS_MESSAGE_PATH = TEST_ROOT / "status_message.json" COURSES_ROOT = TEST_ROOT / "data" DATA_DIR = COURSES_ROOT -LOGGING = get_logger_config(TEST_ROOT / "log", - logging_env="dev", - tracking_filename="tracking.log", - dev_env=True, - debug=True) - COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # Where the content data is checked out. This may not exist on jenkins. GITHUB_REPO_ROOT = ENV_ROOT / "data" @@ -65,6 +59,10 @@ XQUEUE_INTERFACE = { } XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds + +# Don't rely on a real staff grading backend +MOCK_STAFF_GRADING = True + # TODO (cpennington): We need to figure out how envs/test.py can inject things # into common.py so that we don't have to repeat this sort of thing STATICFILES_DIRS = [ @@ -99,7 +97,7 @@ DATABASES = { } CACHES = { - # This is the cache used for most things. + # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', diff --git a/lms/static/admin/css/ie.css b/lms/static/admin/css/ie.css deleted file mode 100644 index fd00f7f204..0000000000 --- a/lms/static/admin/css/ie.css +++ /dev/null @@ -1,63 +0,0 @@ -/* IE 6 & 7 */ - -/* Proper fixed width for dashboard in IE6 */ - -.dashboard #content { - *width: 768px; -} - -.dashboard #content-main { - *width: 535px; -} - -/* IE 6 ONLY */ - -/* Keep header from flowing off the page */ - -#container { - _position: static; -} - -/* Put the right sidebars back on the page */ - -.colMS #content-related { - _margin-right: 0; - _margin-left: 10px; - _position: static; -} - -/* Put the left sidebars back on the page */ - -.colSM #content-related { - _margin-right: 10px; - _margin-left: -115px; - _position: static; -} - -.form-row { - _height: 1%; -} - -/* Fix right margin for changelist filters in IE6 */ - -#changelist-filter ul { - _margin-right: -10px; -} - -/* IE ignores min-height, but treats height as if it were min-height */ - -.change-list .filtered { - _height: 400px; -} - -/* IE doesn't know alpha transparency in PNGs */ - -.inline-deletelink { - background: transparent url(../img/inline-delete-8bit.png) no-repeat; -} - -/* IE7 doesn't support inline-block */ -.change-list ul.toplinks li { - zoom: 1; - *display: inline; -} \ No newline at end of file diff --git a/lms/static/coffee/src/main.coffee b/lms/static/coffee/src/main.coffee index ec5cbdec5b..df4c8861f6 100644 --- a/lms/static/coffee/src/main.coffee +++ b/lms/static/coffee/src/main.coffee @@ -32,8 +32,18 @@ $ -> $('#login').click -> $('#login_form input[name="email"]').focus() + _gaq.push(['_trackPageview', '/login']) false $('#signup').click -> $('#signup-modal input[name="email"]').focus() + _gaq.push(['_trackPageview', '/signup']) false + + # fix for ie + if !Array::indexOf + Array::indexOf = (obj, start = 0) -> + for ele, i in this[start..] + if ele is obj + return i + start + return -1 diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee new file mode 100644 index 0000000000..7039fd1358 --- /dev/null +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -0,0 +1,404 @@ +# wrap everything in a class in case we want to use inside xmodules later + +get_random_int: (min, max) -> + return Math.floor(Math.random() * (max - min + 1)) + min + +# states +state_grading = "grading" +state_graded = "graded" +state_no_data = "no_data" +state_error = "error" + +class StaffGradingBackend + constructor: (ajax_url, mock_backend) -> + @ajax_url = ajax_url + @mock_backend = mock_backend + if @mock_backend + @mock_cnt = 0 + + mock: (cmd, data) -> + # Return a mock response to cmd and data + # should take a location as an argument + if cmd == 'get_next' + @mock_cnt++ + switch data.location + when 'i4x://MITx/3.091x/problem/open_ended_demo1' + response = + success: true + problem_name: 'Problem 1' + num_graded: 3 + min_for_ml: 5 + num_pending: 4 + prompt: ''' +

    S11E3: Metal Bands

    +

    Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.

    + +

    * Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?

    +

    This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.

    + ''' + submission: ''' + Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + ''' + rubric: ''' +
      +
    • Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.
    • +
    • Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.
    • +
    • Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.
    • +
    + +

    Please score your response according to how many of the above components you identified:

    + ''' + submission_id: @mock_cnt + max_score: 2 + @mock_cnt % 3 + ml_error_info : 'ML accuracy info: ' + @mock_cnt + when 'i4x://MITx/3.091x/problem/open_ended_demo2' + response = + success: true + problem_name: 'Problem 2' + num_graded: 2 + min_for_ml: 5 + num_pending: 4 + prompt: 'This is a fake second problem' + submission: 'This is the best submission ever! ' + @mock_cnt + rubric: 'I am a rubric for grading things! ' + @mock_cnt + submission_id: @mock_cnt + max_score: 2 + @mock_cnt % 3 + ml_error_info : 'ML accuracy info: ' + @mock_cnt + else + response = + success: false + + + else if cmd == 'save_grade' + console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") + response = + @mock('get_next', {location: data.location}) + # get_problem_list + # should get back a list of problem_ids, problem_names, num_graded, min_for_ml + else if cmd == 'get_problem_list' + @mock_cnt = 1 + response = + success: true + problem_list: [ + {location: 'i4x://MITx/3.091x/problem/open_ended_demo1', \ + problem_name: "Problem 1", num_graded: 3, num_pending: 5, min_for_ml: 10}, + {location: 'i4x://MITx/3.091x/problem/open_ended_demo2', \ + problem_name: "Problem 2", num_graded: 1, num_pending: 5, min_for_ml: 10} + ] + else + response = + success: false + error: 'Unknown command ' + cmd + + if @mock_cnt % 5 == 0 + response = + success: true + message: 'No more submissions' + + + if @mock_cnt % 7 == 0 + response = + success: false + error: 'An error for testing' + + return response + + + post: (cmd, data, callback) -> + if @mock_backend + callback(@mock(cmd, data)) + else + # TODO: replace with postWithPrefix when that's loaded + $.post(@ajax_url + cmd, data, callback) + .error => callback({success: false, error: "Error occured while performing this operation"}) + + +class StaffGrading + constructor: (backend) -> + @backend = backend + + # all the jquery selectors + + @problem_list_container = $('.problem-list-container') + @problem_list = $('.problem-list') + + @error_container = $('.error-container') + @message_container = $('.message-container') + + @prompt_name_container = $('.prompt-name') + @prompt_container = $('.prompt-container') + @prompt_wrapper = $('.prompt-wrapper') + + @submission_container = $('.submission-container') + @submission_wrapper = $('.submission-wrapper') + + @rubric_container = $('.rubric-container') + @rubric_wrapper = $('.rubric-wrapper') + @grading_wrapper = $('.grading-wrapper') + + @feedback_area = $('.feedback-area') + @score_selection_container = $('.score-selection-container') + + @submit_button = $('.submit-button') + @action_button = $('.action-button') + @skip_button = $('.skip-button') + + @problem_meta_info = $('.problem-meta-info-container') + @meta_info_wrapper = $('.meta-info-wrapper') + @ml_error_info_container = $('.ml-error-info-container') + + @breadcrumbs = $('.breadcrumbs') + + # model state + @state = state_no_data + @submission_id = null + @prompt = '' + @submission = '' + @rubric = '' + @error_msg = '' + @message = '' + @max_score = 0 + @ml_error_info= '' + @location = '' + @prompt_name = '' + @min_for_ml = 0 + @num_graded = 0 + @num_pending = 0 + + @score = null + @problems = null + + # action handlers + @submit_button.click @submit + # TODO: fix this to do something more intelligent + @action_button.click @submit + @skip_button.click @skip_and_get_next + + # send initial request automatically + @get_problem_list() + + + setup_score_selection: => + # first, get rid of all the old inputs, if any. + @score_selection_container.html('Choose score: ') + + # Now create new labels and inputs for each possible score. + for score in [0..@max_score] + id = 'score-' + score + label = """""" + + input = """ + + """ # " fix broken parsing in emacs + @score_selection_container.append(input + label) + + # And now hook up an event handler again + $("input[name='score-selection']").change @graded_callback + + + set_button_text: (text) => + @action_button.attr('value', text) + + graded_callback: (event) => + @score = event.target.value + @state = state_graded + @message = '' + @render_view() + + ajax_callback: (response) => + # always clear out errors and messages on transition. + @error_msg = '' + @message = '' + + if response.success + if response.problem_list + @problems = response.problem_list + else if response.submission + @data_loaded(response) + else + @no_more(response.message) + else + @error(response.error) + + @render_view() + + get_next_submission: (location) -> + @location = location + @list_view = false + @backend.post('get_next', {location: location}, @ajax_callback) + + skip_and_get_next: () => + data = + score: @score + feedback: @feedback_area.val() + submission_id: @submission_id + location: @location + skipped: true + @backend.post('save_grade', data, @ajax_callback) + + get_problem_list: () -> + @list_view = true + @backend.post('get_problem_list', {}, @ajax_callback) + + submit_and_get_next: () -> + data = + score: @score + feedback: @feedback_area.val() + submission_id: @submission_id + location: @location + + @backend.post('save_grade', data, @ajax_callback) + + error: (msg) -> + @error_msg = msg + @state = state_error + + data_loaded: (response) -> + @prompt = response.prompt + @submission = response.submission + @rubric = response.rubric + @submission_id = response.submission_id + @feedback_area.val('') + @max_score = response.max_score + @score = null + @ml_error_info=response.ml_error_info + @prompt_name = response.problem_name + @num_graded = response.num_graded + @min_for_ml = response.min_for_ml + @num_pending = response.num_pending + @state = state_grading + if not @max_score? + @error("No max score specified for submission.") + + no_more: (message) -> + @prompt = null + @prompt_name = '' + @num_graded = 0 + @min_for_ml = 0 + @submission = null + @rubric = null + @ml_error_info = null + @submission_id = null + @message = message + @score = null + @max_score = 0 + @state = state_no_data + + + render_view: () -> + # clear the problem list and breadcrumbs + @problem_list.html('') + @breadcrumbs.html('') + @problem_list_container.toggle(@list_view) + if @backend.mock_backend + @message = @message + "

    NOTE: Mocking backend.

    " + @message_container.html(@message) + @error_container.html(@error_msg) + @message_container.toggle(@message != "") + @error_container.toggle(@error_msg != "") + + + # only show the grading elements when we are not in list view or the state + # is invalid + show_grading_elements = !(@list_view || @state == state_error || + @state == state_no_data) + @prompt_wrapper.toggle(show_grading_elements) + @submission_wrapper.toggle(show_grading_elements) + @rubric_wrapper.toggle(show_grading_elements) + @grading_wrapper.toggle(show_grading_elements) + @meta_info_wrapper.toggle(show_grading_elements) + @action_button.hide() + + if @list_view + @render_list() + else + @render_problem() + + problem_link:(problem) -> + link = $('').attr('href', "javascript:void(0)").append( + "#{problem.problem_name} (#{problem.num_graded} graded, #{problem.num_pending} pending)") + .click => + @get_next_submission problem.location + + make_paragraphs: (text) -> + paragraph_split = text.split(/\n\s*\n/) + new_text = '' + for paragraph in paragraph_split + new_text += "

    #{paragraph}

    " + return new_text + + render_list: () -> + for problem in @problems + @problem_list.append($('
  • ').append(@problem_link(problem))) + + render_problem: () -> + # make the view elements match the state. Idempotent. + show_submit_button = true + show_action_button = true + + problem_list_link = $('').attr('href', 'javascript:void(0);') + .append("< Back to problem list") + .click => @get_problem_list() + + # set up the breadcrumbing + @breadcrumbs.append(problem_list_link) + + + if @state == state_error + @set_button_text('Try loading again') + show_action_button = true + + else if @state == state_grading + @ml_error_info_container.html(@ml_error_info) + meta_list = $(" diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index 833a237251..415199141d 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -6,7 +6,25 @@ ## EdX Blog - 2012-10-14T14:08:12-07:00 + 2012-12-19T14:00:12-07:00 + + tag:www.edx.org,2012:Post/10 + 2012-12-19T14:00:00-07:00 + 2012-12-19T14:00:00-07:00 + + edX announces first wave of new courses for Spring 2013 + <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> + <p></p> + + + tag:www.edx.org,2012:Post/9 + 2012-12-10T14:00:00-07:00 + 2012-12-10T14:00:00-07:00 + + Georgetown University joins edX + <img src="${static.url('images/press/releases/georgetown-seal_240x180.png')}" /> + <p>Sixth institution to join global movement in year one</p> + tag:www.edx.org,2012:Post/8 2012-12-04T14:00:00-07:00 diff --git a/lms/templates/index.html b/lms/templates/index.html index 05fef3dffa..d82c9120d4 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -48,7 +48,7 @@

    Explore free courses from edX universities

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

    Staff grading

    + +
    +
    +
    +
    +
    +

    Instructions

    +
    +

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

    +
    + +

    Problem List

    +
      +
    +
    + +
    +

    +
    +

    Problem Information

    +
    +
    +

    Maching Learning Information

    +
    +
    +
    +
    +

    Question

    +
    +
    +
    +
    +

    Grading Rubric

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

    Grading

    + +
    +
    +

    Student Submission

    +
    +
    +
    +
    +

    +

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

    Score: ${score}

    + % if grader_type == "ML": +

    Check below for full feedback:

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

    Organization

    -

    What is edX?

    -

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

    -

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

    +

    What is edX?

    +

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

    +

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

    -
    -

    Why is Wellesley College joining edX?

    -

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

    -

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

    -

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

    -
    -
    -

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

    -

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

    -
    -
    -

    How many WellesleyX courses will be offered initially? When?

    -

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

    -

    Will edX be adding additional X Universities?

    -

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

    -

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

    +

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

    +

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

    Students

    -

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

    -

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

    +

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

    +

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

    -

    Will certificates be awarded?

    -

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

    +

    Will certificates be awarded?

    +

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

    -

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

    -

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

    +

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

    +

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

    -

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

    -

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

    +

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

    +

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

    -

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

    -

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

    +

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

    +

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

    -

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

    -

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

    +

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

    +

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

    -

    How may I apply to study with edX?

    -

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

    +

    How may I apply to study with edX?

    +

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

    -

    How may another university participate in edX?

    -

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

    +

    How may another university participate in edX?

    +

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

    Technology Platform

    -

    What technology will edX use?

    -

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

    -

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

    +

    What technology will edX use?

    +

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

    +

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

    -

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

    -

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

    +

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

    +

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

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

    Do You Want to Change the Future of Education?

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

    We're hiring!

    -

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

    -

    Want to apply to edX?

    -

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

    -

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

    +

    EdX is looking to add new talent to our team!

    +

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

    +

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

    +

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

    +

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

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

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

    +
    + +
    +
    +

    INSTRUCTIONAL DESIGNER — CONTRACT OPPORTUNITY

    +

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

    +

    Responsibilities:

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

    Qualifications:

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

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

    +

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

    +
    +
    + +
    +
    +

    MEMBER SERVICES MANAGER

    +

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

    +

    Responsibilities:

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

    Qualifications:

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

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

    +
    +
    + +
    +
    +

    DIRECTOR OF PR AND COMMUNICATIONS

    +

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

    +

    Responsibilities:

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

    Qualifications:

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

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

    +
    +
    +
    +
    - - - - +

    Positions

    +

    How to Apply

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

    Our Location

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

    Georgetown University joins edX

    +
    +
    +

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

    + +

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

    + +

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

    + +

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

    + +

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

    + +

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

    + +

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

    + +

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

    + +

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

    + +

    About edX

    + +

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

    + +

    About Georgetown University

    + +

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

    + +
    +

    Contact: Brad Baker

    +

    BBaker@webershandwick.com

    +

    617-520-7043

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

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

    +
    + +
    +

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

    + +

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

    + +

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

    + +

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

    + + + +

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

    + +

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

    + +

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

    + +

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

    + +

    About edX

    + +

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

    + +
    +

    Contact: Brad Baker

    +

    BBaker@webershandwick.com

    +

    617-520-7260

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

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

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

    ${display_name}

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