def handle(self, *args, **options): source_key = SlashSeparatedCourseKey.from_deprecated_string(options['source_course']) dest_key = SlashSeparatedCourseKey.from_deprecated_string(options['dest_course']) source_students = User.objects.filter( courseenrollment__course_id=source_key ) for user in source_students: if CourseEnrollment.is_enrolled(user, dest_key): # Un Enroll from source course but don't mess # with the enrollment in the destination course. CourseEnrollment.unenroll(user, source_key) print("Unenrolled {} from {}".format(user.username, source_key.to_deprecated_string())) msg = "Skipping {}, already enrolled in destination course {}" print(msg.format(user.username, dest_key.to_deprecated_string())) continue print("Moving {}.".format(user.username)) # Find the old enrollment. enrollment = CourseEnrollment.objects.get( user=user, course_id=source_key ) # Move the Student between the classes. mode = enrollment.mode old_is_active = enrollment.is_active CourseEnrollment.unenroll(user, source_key) new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode) # Unenroll from the new coures if the user had unenrolled # form the old course. if not old_is_active: new_enrollment.update_enrollment(is_active=False) if mode == 'verified': try: certificate_item = CertificateItem.objects.get( course_id=source_key, course_enrollment=enrollment ) except CertificateItem.DoesNotExist: print("No certificate for {}".format(user)) continue certificate_item.course_id = dest_key certificate_item.course_enrollment = new_enrollment certificate_item.save()
def progress(request, course_id, student_id=None): """ Wraps "_progress" with the manual_transaction context manager just in case there are unanticipated errors. """ with grades.manual_transaction(): return _progress(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), student_id)
def course_info(request, course_id): """ Display the course's info.html, or 404 if there is no such course. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) staff_access = has_access(request.user, 'staff', course) masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page reverifications = fetch_reverify_banner_info(request, course_key) studio_url = get_studio_url(course_key, 'course_info') context = { 'request': request, 'course_id': course_key.to_deprecated_string(), 'cache': None, 'course': course, 'staff_access': staff_access, 'masquerade': masq, 'studio_url': studio_url, 'reverifications': reverifications, } return render_to_response('courseware/info.html', context)
def handle(self, *args, **options): if not options["course"]: raise CommandError(Command.course_option.help) try: course_key = CourseKey.from_string(options["course"]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options["course"]) course = get_course_by_id(course_key) print "Warning: this command directly edits the list of course tabs in mongo." print "Tabs before any changes:" print_course(course) try: if options["delete"]: if len(args) != 1: raise CommandError(Command.delete_option.help) num = int(args[0]) if query_yes_no("Deleting tab {0} Confirm?".format(num), default="no"): tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing elif options["insert"]: if len(args) != 3: raise CommandError(Command.insert_option.help) num = int(args[0]) tab_type = args[1] name = args[2] if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default="no"): tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above except ValueError as e: # Cute: translate to CommandError so the CLI error prints nicely. raise CommandError(e)
def handle(self, *args, **options): print "args = ", args if len(args) > 0: course_id = args[0] else: print self.help return course_key = None # parse out the course id into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: course = get_course_by_id(course_key) except Exception as err: print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Computing grades for {}".format(course_id) offline_grade_calculation(course_key)
def initdb(self, default): """ Initialize the database and create one test course in it """ # set the default modulestore self.options['stores']['default'] = self.options['stores'][default] self.store = MixedModuleStore(**self.options) self.addCleanup(self.store.close_all_connections) # convert to CourseKeys self.course_locations = { course_id: SlashSeparatedCourseKey.from_deprecated_string(course_id) for course_id in [self.MONGO_COURSEID, self.XML_COURSEID1, self.XML_COURSEID2] } # and then to the root UsageKey self.course_locations = { course_id: course_key.make_usage_key('course', course_key.run) for course_id, course_key in self.course_locations.iteritems() # pylint: disable=maybe-no-member } self.fake_location = Location('foo', 'bar', 'slowly', 'vertical', 'baz') self.import_chapter_location = self.course_locations[ self.MONGO_COURSEID].replace(category='chapter', name='Overview') self.xml_chapter_location = self.course_locations[ self.XML_COURSEID1].replace(category='chapter', name='Overview') # get Locators and set up the loc mapper if app is Locator based if default == 'split': self.fake_location = loc_mapper().translate_location( self.fake_location) self._create_course( default, self.course_locations[self.MONGO_COURSEID].course_key)
def get(self, request, *args, **kwargs): """Shows logs of imports that happened as a result of a git import""" course_id = kwargs.get('course_id') if course_id: course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Set mongodb defaults even if it isn't defined in settings mongo_db = { 'host': 'localhost', 'user': '', 'password': '', 'db': 'xlog', } # Allow overrides if hasattr(settings, 'MONGODB_LOG'): for config_item in ['host', 'user', 'password', 'db', ]: mongo_db[config_item] = settings.MONGODB_LOG.get( config_item, mongo_db[config_item]) mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db) error_msg = '' try: if mongo_db['user'] and mongo_db['password']: mdb = mongoengine.connect(mongo_db['db'], host=mongouri) else: mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host']) except mongoengine.connection.ConnectionError: log.exception('Unable to connect to mongodb to save log, ' 'please check MONGODB_LOG settings.') if course_id is None: # Require staff if not going to specific course if not request.user.is_staff: raise Http404 cilset = CourseImportLog.objects.all().order_by('-created') else: try: course = get_course_by_id(course_id) except Exception: # pylint: disable=broad-except log.info('Cannot find course {0}'.format(course_id)) raise Http404 # Allow only course team, instructors, and staff if not (request.user.is_staff or CourseInstructorRole(course.id).has_user(request.user) or CourseStaffRole(course.id).has_user(request.user)): raise Http404 log.debug('course_id={0}'.format(course_id)) cilset = CourseImportLog.objects.filter(course_id=course_id).order_by('-created') log.debug('cilset length={0}'.format(len(cilset))) mdb.disconnect() context = {'cilset': cilset, 'course_id': course_id.to_deprecated_string() if course_id else None, 'error_msg': error_msg} return render_to_response(self.template_name, context)
def index(request, course_id, book_index, page=None): """ Serve static image-based textbooks. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) staff_access = has_access(request.user, 'staff', course) book_index = int(book_index) if book_index < 0 or book_index >= len(course.textbooks): raise Http404("Invalid book index value: {0}".format(book_index)) textbook = course.textbooks[book_index] table_of_contents = textbook.table_of_contents if page is None: page = textbook.start_page return render_to_response( 'staticbook.html', { 'book_index': book_index, 'page': int(page), 'course': course, 'book_url': textbook.book_url, 'table_of_contents': table_of_contents, 'start_page': textbook.start_page, 'end_page': textbook.end_page, 'staff_access': staff_access, }, )
def handle(self, *args, **options): if len(args) != 1: raise CommandError("Usage: unique_id_mapping %s" % " ".join(("<%s>" % arg for arg in Command.args))) course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) # Generate the output filename from the course ID. # Change slashes to dashes first, and then append .csv extension. output_filename = course_key.to_deprecated_string().replace('/', '-') + ".csv" # Figure out which students are enrolled in the course students = User.objects.filter(courseenrollment__course_id=course_key) if len(students) == 0: self.stdout.write("No students enrolled in %s" % course_key.to_deprecated_string()) return # Write mapping to output file in CSV format with a simple header try: with open(output_filename, 'wb') as output_file: csv_writer = csv.writer(output_file) csv_writer.writerow(( "User ID", "Per-Student anonymized user ID", "Per-course anonymized user id" )) for student in students: csv_writer.writerow(( student.id, anonymous_id_for_user(student, None), anonymous_id_for_user(student, course_key) )) except IOError: raise CommandError("Error writing to file: %s" % output_filename)
def handle(self, *args, **options): """Handler for command.""" task_number = options['task_number'] if len(args) == 2: course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) usage_key = course_id.make_usage_key_from_deprecated_string(args[1]) else: print self.help return try: course = get_course(course_id) except ValueError as err: print err return descriptor = modulestore().get_item(usage_key, depth=0) if descriptor is None: print "Location {0} not found in course".format(usage_key) return try: enrolled_students = CourseEnrollment.users_enrolled_in(course_id) print "Total students enrolled in {0}: {1}".format(course_id, enrolled_students.count()) calculate_task_statistics(enrolled_students, course, usage_key, task_number) except KeyboardInterrupt: print "\nOperation Cancelled"
def section_problem_grade_distrib(request, course_id, section): """ Creates a json with the grade distribution for the problems in the specified section. `request` django request `course_id` the course ID for the course interested in `section` The zero-based index of the section for the course Returns the format in dashboard_data.get_d3_section_grade_distrib If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution and pick out the sections of interest. """ json = {} # Only instructor for this particular course can request this information course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) if has_instructor_access_for_class(request.user, course_key): try: json = dashboard_data.get_d3_section_grade_distrib(course_key, section) except Exception as ex: # pylint: disable=broad-except log.error('Generating metrics failed with exception: %s', ex) json = {'error': "error"} else: json = {'error': "Access Denied: User does not have access to this course's data"} return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def all_sequential_open_distrib(request, course_id): """ Creates a json with the open distribution for all the subsections in the course. `request` django request `course_id` the course ID for the course interested in Returns the format in dashboard_data.get_d3_sequential_open_distrib """ json = {} # Only instructor for this particular course can request this information course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) if has_instructor_access_for_class(request.user, course_key): try: json = dashboard_data.get_d3_sequential_open_distrib(course_key) except Exception as ex: # pylint: disable=broad-except log.error('Generating metrics failed with exception: %s', ex) json = {'error': "error"} else: json = {'error': "Access Denied: User does not have access to this course's data"} return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def initdb(self, default): """ Initialize the database and create one test course in it """ # set the default modulestore self.options['stores']['default'] = self.options['stores'][default] self.store = MixedModuleStore(**self.options) self.addCleanup(self.store.close_all_connections) # convert to CourseKeys self.course_locations = { course_id: SlashSeparatedCourseKey.from_deprecated_string(course_id) for course_id in [self.MONGO_COURSEID, self.XML_COURSEID1, self.XML_COURSEID2] } # and then to the root UsageKey self.course_locations = { course_id: course_key.make_usage_key('course', course_key.run) for course_id, course_key in self.course_locations.iteritems() # pylint: disable=maybe-no-member } self.fake_location = Location('foo', 'bar', 'slowly', 'vertical', 'baz') self.import_chapter_location = self.course_locations[self.MONGO_COURSEID].replace( category='chapter', name='Overview' ) self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace( category='chapter', name='Overview' ) # get Locators and set up the loc mapper if app is Locator based if default == 'split': self.fake_location = loc_mapper().translate_location(self.fake_location) self._create_course(default, self.course_locations[self.MONGO_COURSEID].course_key)
def show_requirements(request, course_id): """ Show the requirements necessary for the verification flow. """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) upgrade = request.GET.get('upgrade', False) course = course_from_id(course_id) context = { "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}), "verify_student_url": reverse('verify_student_verify', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "is_not_active": not request.user.is_active, "upgrade": upgrade, } return render_to_response("verify_student/show_requirements.html", context)
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.build_absolute_uri()) course_id = None if match: course_id = match.group('course_id') course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) context = {} if course_id: context['course_id'] = course_id if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_key, ).values_list('key', 'value') ) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context( self.CONTEXT_NAME, context )
def add_course_to_cart(request, course_id): """ Adds course specified by course_id to the cart. The model function add_to_order does all the heavy lifting (logging, error checking, etc) """ assert isinstance(course_id, basestring) if not request.user.is_authenticated(): log.info("Anon user trying to add course {} to cart".format(course_id)) return HttpResponseForbidden( _('You must be logged-in to add to a shopping cart')) cart = Order.get_cart_for_user(request.user) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # All logging from here handled by the model try: PaidCourseRegistration.add_to_order(cart, course_key) except CourseDoesNotExistException: return HttpResponseNotFound( _('The course you requested does not exist.')) except ItemAlreadyInCartException: return HttpResponseBadRequest( _('The course {0} is already in your cart.'.format(course_id))) except AlreadyEnrolledInCourseException: return HttpResponseBadRequest( _('You are already registered in course {0}.'.format(course_id))) return HttpResponse(_("Course added to cart."))
def spoc_gradebook(request, course_id): """ Show the gradebook for this course: - Only shown for courses with enrollment < settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") - Only displayed to course staff """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key, courseenrollment__is_active=1 ).order_by('username').select_related("profile") # possible extension: implement pagination to show to large courses student_info = [ { 'username': student.username, 'id': student.id, 'email': student.email, 'grade_summary': student_grades(student, request, course), 'realname': student.profile.name, } for student in enrolled_students ] return render_to_response('courseware/gradebook.html', { 'students': student_info, 'course': course, 'course_id': course_key, # Checked above 'staff_access': True, 'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True), })
def parse_args(self, *args): """ Return a 4-tuple of (course_key, user, org, offering). If the user didn't specify an org & offering, those will be None. """ if len(args) < 2: raise CommandError( "migrate_to_split requires at least two arguments: " "a course_key and a user identifier (email or ID)" ) try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) try: user = user_from_str(args[1]) except User.DoesNotExist: raise CommandError("No user found identified by {}".format(args[1])) try: org = args[2] offering = args[3] except IndexError: org = offering = None return course_key, user, org, offering
def post(self, request): """Handle all actions from courses view""" if not request.user.is_staff: raise Http404 action = request.POST.get('action', '') track.views.server_track(request, action, {}, page='courses_sysdashboard') courses = {course.id: course for course in self.get_courses()} if action == 'add_course': gitloc = request.POST.get('repo_location', '').strip().replace(' ', '').replace(';', '') branch = request.POST.get('repo_branch', '').strip().replace(' ', '').replace(';', '') self.msg += self.get_course_from_git(gitloc, branch) elif action == 'del_course': course_id = request.POST.get('course_id', '').strip() course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_found = False if course_key in courses: course_found = True course = courses[course_key] else: try: course = get_course_by_id(course_key) course_found = True except Exception, err: # pylint: disable=broad-except self.msg += _( 'Error - cannot get course with ID {0}<br/><pre>{1}</pre>' ).format( course_key, escape(str(err)) ) is_xml_course = (modulestore().get_modulestore_type(course_key) == XML_MODULESTORE_TYPE) if course_found and is_xml_course: cdir = course.data_dir self.def_ms.courses.pop(cdir) # now move the directory (don't actually delete it) new_dir = "{course_dir}_deleted_{timestamp}".format( course_dir=cdir, timestamp=int(time.time()) ) os.rename(settings.DATA_DIR / cdir, settings.DATA_DIR / new_dir) self.msg += (u"<font color='red'>Deleted " u"{0} = {1} ({2})</font>".format( cdir, course.id, course.display_name)) elif course_found and not is_xml_course: # delete course that is stored with mongodb backend content_store = contentstore() commit = True delete_course(self.def_ms, content_store, course.id, commit) # don't delete user permission groups, though self.msg += \ u"<font color='red'>{0} {1} = {2} ({3})</font>".format( _('Deleted'), course.location.to_deprecated_string(), course.id.to_deprecated_string(), course.display_name)
def setUp(self): self.course_name = 'edX/toy/2012_Fall' # Create student account student = UserFactory.create() CourseEnrollmentFactory.create( user=student, course_id=SlashSeparatedCourseKey.from_deprecated_string(self.course_name) ) self.client.login(username=student.username, password="******") try: # URL for dashboard self.url = reverse('dashboard') except NoReverseMatch: raise SkipTest("Skip this test if url cannot be found (ie running from CMS tests)") # URL for email settings modal self.email_modal_link = ( ('<a href="#email-settings-modal" class="email-settings" rel="leanModal" ' 'data-course-id="{0}/{1}/{2}" data-course-number="{1}" ' 'data-optout="False">Email Settings</a>').format( 'edX', 'toy', '2012_Fall' ) )
def handle(self, *args, **options): """Handler for command.""" task_number = options['task_number'] if len(args) == 2: course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) usage_key = course_id.make_usage_key_from_deprecated_string( args[1]) else: print self.help return try: course = get_course(course_id) except ValueError as err: print err return descriptor = modulestore().get_item(usage_key, depth=0) if descriptor is None: print "Location {0} not found in course".format(usage_key) return try: enrolled_students = CourseEnrollment.users_enrolled_in(course_id) print "Total students enrolled in {0}: {1}".format( course_id, enrolled_students.count()) calculate_task_statistics(enrolled_students, course, usage_key, task_number) except KeyboardInterrupt: print "\nOperation Cancelled"
def handle(self, *args, **options): print "args = ", args if len(args) > 0: course_id = args[0] else: print self.help return course_key = None # parse out the course id into a coursekey try: course_key = CourseKey.from_string(course_id) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( course_id) try: _course = get_course_by_id(course_key) except Exception as err: print "-----------------------------------------------------------------------------" print "Sorry, cannot find course with id {}".format(course_id) print "Got exception {}".format(err) print "Please provide a course ID or course data directory name, eg content-mit-801rq" return print "-----------------------------------------------------------------------------" print "Computing grades for {}".format(course_id) offline_grade_calculation(course_key)
def handle(self, *args, **options): """ Checks arguments and runs export function if they are good """ if len(args) != 2: raise CommandError('This script requires exactly two arguments: ' 'course_loc and git_url') # Rethrow GitExportError as CommandError for SystemExit try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[0]) except InvalidKeyError: raise CommandError(GitExportError.BAD_COURSE) try: git_export_utils.export_to_git(course_key, args[1], options.get('user', ''), options.get('rdir', None)) except git_export_utils.GitExportError as ex: raise CommandError(str(ex))
def handle(self, *args, **options): if len(args) != 1: raise CommandError("Usage: unique_id_mapping %s" % " ".join( ("<%s>" % arg for arg in Command.args))) course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) # Generate the output filename from the course ID. # Change slashes to dashes first, and then append .csv extension. output_filename = course_key.to_deprecated_string().replace( '/', '-') + ".csv" # Figure out which students are enrolled in the course students = User.objects.filter(courseenrollment__course_id=course_key) if len(students) == 0: self.stdout.write("No students enrolled in %s" % course_key.to_deprecated_string()) return # Write mapping to output file in CSV format with a simple header try: with open(output_filename, 'wb') as output_file: csv_writer = csv.writer(output_file) csv_writer.writerow( ("User ID", "Per-Student anonymized user ID", "Per-course anonymized user id")) for student in students: csv_writer.writerow( (student.id, anonymous_id_for_user(student, None), anonymous_id_for_user(student, course_key))) except IOError: raise CommandError("Error writing to file: %s" % output_filename)
def handle(self, *args, **options): """ Checks arguments and runs export function if they are good """ if len(args) != 2: raise CommandError('This script requires exactly two arguments: ' 'course_loc and git_url') # Rethrow GitExportError as CommandError for SystemExit try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0]) except InvalidKeyError: raise CommandError(GitExportError.BAD_COURSE) try: git_export_utils.export_to_git( course_key, args[1], options.get('user', ''), options.get('rdir', None) ) except git_export_utils.GitExportError as ex: raise CommandError(str(ex))
def request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ if request.method == "POST": if request.user.is_authenticated(): xqci = XQueueCertInterface() username = request.user.username student = User.objects.get(username=username) course_key = SlashSeparatedCourseKey.from_deprecated_string( request.POST.get('course_id')) course = modulestore().get_course(course_key, depth=2) status = certificate_status_for_student(student, course_key)['status'] if status in [ CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error ]: logger.info( 'Grading and certification requested for user {} in course {} via /request_certificate call' .format(username, course_key)) status = xqci.add_cert(student, course_key, course=course) return HttpResponse(json.dumps({'add_status': status}), mimetype='application/json') return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), mimetype='application/json')
def jump_to(request, course_id, location): """ Show the page that contains a specific location. If the location is invalid or not in any class, return a 404. Otherwise, delegates to the index view to figure out whether this user has access, and what they should see. """ try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) usage_key = course_key.make_usage_key_from_deprecated_string(location) except InvalidKeyError: raise Http404(u"Invalid course_key or usage_key") try: (course_key, chapter, section, position) = path_to_location(modulestore(), usage_key) except ItemNotFoundError: raise Http404(u"No data at this location: {0}".format(usage_key)) except NoPathToItem: raise Http404(u"This location is not in any class: {0}".format(usage_key)) # 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: return redirect('courseware', course_id=course_key.to_deprecated_string()) elif section is None: return redirect('courseware_chapter', course_id=course_key.to_deprecated_string(), chapter=chapter) elif position is None: return redirect('courseware_section', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section) else: return redirect('courseware_position', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section, position=position)
def test_dump_course_structure(self): args = [TEST_COURSE_ID] kwargs = {'modulestore': 'default'} output = self.call_command('dump_course_structure', *args, **kwargs) dump = json.loads(output) # check that all elements in the course structure have metadata, # but not inherited metadata: for element in dump.itervalues(): self.assertIn('metadata', element) self.assertIn('children', element) self.assertIn('category', element) self.assertNotIn('inherited_metadata', element) # Check a few elements in the course dump test_course_key = SlashSeparatedCourseKey.from_deprecated_string(TEST_COURSE_ID) parent_id = test_course_key.make_usage_key('chapter', 'Overview').to_deprecated_string() self.assertEqual(dump[parent_id]['category'], 'chapter') self.assertEqual(len(dump[parent_id]['children']), 3) child_id = dump[parent_id]['children'][1] self.assertEqual(dump[child_id]['category'], 'videosequence') self.assertEqual(len(dump[child_id]['children']), 2) video_id = test_course_key.make_usage_key('video', 'Welcome').to_deprecated_string() self.assertEqual(dump[video_id]['category'], 'video') self.assertEqual(len(dump[video_id]['metadata']), 4) self.assertIn('youtube_id_1_0', dump[video_id]['metadata']) # Check if there are the right number of elements self.assertEqual(len(dump), 16)
def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents( request, course, tab ) if contents is None: raise Http404 return render_to_response('courseware/static_tab.html', { 'course': course, 'tab': tab, 'tab_contents': contents, })
def handle(self, *args, **options): if len(args) != 1: raise CommandError("course_id not specified") # Get the modulestore try: name = options['modulestore'] store = modulestore(name) except KeyError: raise CommandError("Unknown modulestore {}".format(name)) # Get the course data try: course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) except InvalidKeyError: raise CommandError("Invalid course_id") course = store.get_course(course_id) if course is None: raise CommandError("Invalid course_id") # precompute inherited metadata at the course level, if needed: if options['inherited']: compute_inherited_metadata(course) # Convert course data to dictionary and dump it as JSON to stdout info = dump_module(course, inherited=options['inherited'], defaults=options['inherited_defaults']) return json.dumps(info, indent=2, sort_keys=True)
def get_anon_ids(request, course_id): # pylint: disable=W0613 """ Respond with 2-column CSV output of user-id, anonymized-user-id """ # TODO: the User.objects query and CSV generation here could be # centralized into analytics. Currently analytics has similar functionality # but not quite what's needed. course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) def csv_response(filename, header, rows): """Returns a CSV http response for the given header and rows (excel/utf-8).""" response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) # In practice, there should not be non-ascii data in this query, # but trying to do the right thing anyway. encoded = [unicode(s).encode('utf-8') for s in header] writer.writerow(encoded) for row in rows: encoded = [unicode(s).encode('utf-8') for s in row] writer.writerow(encoded) return response students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') header = ['User ID', 'Anonymized user ID', 'Course Specific Anonymized user ID'] rows = [[s.id, unique_id_for_user(s), anonymous_id_for_user(s, course_id)] for s in students] return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
def section_problem_grade_distrib(request, course_id, section): """ Creates a json with the grade distribution for the problems in the specified section. `request` django request `course_id` the course ID for the course interested in `section` The zero-based index of the section for the course Returns the format in dashboard_data.get_d3_section_grade_distrib If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution and pick out the sections of interest. """ json = {} # Only instructor for this particular course can request this information course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) if has_instructor_access_for_class(request.user, course_key): try: json = dashboard_data.get_d3_section_grade_distrib( course_key, section) except Exception as ex: # pylint: disable=broad-except log.error('Generating metrics failed with exception: %s', ex) json = {'error': "error"} else: json = { 'error': "Access Denied: User does not have access to this course's data" } return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def all_problem_grade_distribution(request, course_id): """ Creates a json with the grade distribution for all the problems in the course. `Request` django request `course_id` the course ID for the course interested in Returns the format in dashboard_data.get_d3_problem_grade_distrib """ json = {} # Only instructor for this particular course can request this information course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) if has_instructor_access_for_class(request.user, course_key): try: json = dashboard_data.get_d3_problem_grade_distrib(course_key) except Exception as ex: # pylint: disable=broad-except log.error('Generating metrics failed with exception: %s', ex) json = {'error': "error"} else: json = { 'error': "Access Denied: User does not have access to this course's data" } return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def send_email(request, course_id): """ Send an email to self, staff, or everyone involved in a course. Query Parameters: - 'send_to' specifies what group the email should be sent to Options are defined by the CourseEmail model in lms/djangoapps/bulk_email/models.py - 'subject' specifies email's subject - 'message' specifies email's content """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) if not bulk_email_is_enabled_for_course(course_id): return HttpResponseForbidden("Email is not enabled for this course.") send_to = request.POST.get("send_to") subject = request.POST.get("subject") message = request.POST.get("message") # Create the CourseEmail object. This is saved immediately, so that # any transaction that has been pending up to this point will also be # committed. email = CourseEmail.create(course_id, request.user, send_to, subject, message) # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) instructor_task.api.submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101 response_payload = { 'course_id': course_id.to_deprecated_string(), 'success': True, } return JsonResponse(response_payload)
def add_cohort(request, course_key): """ Return json of dict: {'success': True, 'cohort': {'id': id, 'name': name}} or {'success': False, 'msg': error_msg} if there's an error """ # this is a string when we get it here course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key) get_course_with_access(request.user, 'staff', course_key) name = request.POST.get("name") if not name: return json_http_response({'success': False, 'msg': "No name specified"}) try: cohort = cohorts.add_cohort(course_key, name) except ValueError as err: return json_http_response({'success': False, 'msg': str(err)}) return json_http_response({'success': 'True', 'cohort': { 'id': cohort.id, 'name': cohort.name }})
def handle(self, *args, **options): if len(args) != 1: raise CommandError("course_id not specified") # Get the modulestore try: name = options["modulestore"] store = modulestore(name) except KeyError: raise CommandError("Unknown modulestore {}".format(name)) # Get the course data try: course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) except InvalidKeyError: raise CommandError("Invalid course_id") course = store.get_course(course_id) if course is None: raise CommandError("Invalid course_id") # precompute inherited metadata at the course level, if needed: if options["inherited"]: compute_inherited_metadata(course) # Convert course data to dictionary and dump it as JSON to stdout info = dump_module(course, inherited=options["inherited"], defaults=options["inherited_defaults"]) return json.dumps(info, indent=2, sort_keys=True)
def handle(self, *args, **options): dry_run = options['dry_run'] task_number = options['task_number'] if len(args) == 4: course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) location = course_id.make_usage_key_from_deprecated_string(args[1]) students_ids = [line.strip() for line in open(args[2])] hostname = args[3] else: print self.help return try: course = get_course(course_id) except ValueError as err: print err return descriptor = modulestore().get_item(location, depth=0) if descriptor is None: print "Location not found in course" return if dry_run: print "Doing a dry run." students = User.objects.filter(id__in=students_ids).order_by('username') print "Number of students: {0}".format(students.count()) for student in students: post_submission_for_student(student, course, location, task_number, dry_run=dry_run, hostname=hostname)
def clean_course_id(self): """Validate the course id""" cleaned_id = self.cleaned_data["course_id"] try: course_key = CourseKey.from_string(cleaned_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string( cleaned_id) except InvalidKeyError: msg = u'Course id invalid.' msg += u' --- Entered course id was: "{0}". '.format( cleaned_id) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not modulestore().has_course(course_key): msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format( course_key.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type( course_key) != XML_MODULESTORE_TYPE if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " msg += '"{0}" appears to be an XML backed course.'.format( course_key.to_deprecated_string()) raise forms.ValidationError(msg) return course_key
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.build_absolute_uri()) course_id = None if match: course_id = match.group('course_id') course_key = SlashSeparatedCourseKey.from_deprecated_string( course_id) context = {} if course_id: context['course_id'] = course_id if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_key, ).values_list('key', 'value')) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context(self.CONTEXT_NAME, context)
def clean_course_id(self): """Validate the course id""" cleaned_id = self.cleaned_data["course_id"] try: course_key = CourseKey.from_string(cleaned_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id) except InvalidKeyError: msg = u'Course id invalid.' msg += u' --- Entered course id was: "{0}". '.format(cleaned_id) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not modulestore().has_course(course_key): msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format(course_key.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " msg += '"{0}" appears to be an XML backed course.'.format(course_key.to_deprecated_string()) raise forms.ValidationError(msg) return course_key
def remove_user_from_cohort(request, course_key, cohort_id): """ Expects 'username': username in POST data. Return json dict of: {'success': True} or {'success': False, 'msg': error_msg} """ # this is a string when we get it here course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key) get_course_with_access(request.user, 'staff', course_key) username = request.POST.get('username') if username is None: return json_http_response({'success': False, 'msg': 'No username specified'}) cohort = cohorts.get_cohort_by_id(course_key, cohort_id) try: user = User.objects.get(username=username) cohort.users.remove(user) return json_http_response({'success': True}) except User.DoesNotExist: log.debug('no user') return json_http_response({'success': False, 'msg': "No user '{0}'".format(username)})
def handle(self, *args, **options): if not options['course_id']: raise CommandError("You must specify a course id for this command") if not options['from_mode'] or not options['to_mode']: raise CommandError('You must specify a "to" and "from" mode as parameters') try: course_key = CourseKey.from_string(options['course_id']) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options['course_id']) filter_args = dict( course_id=course_key, mode=options['from_mode'] ) if options['user']: if '@' in options['user']: user = User.objects.get(email=options['user']) else: user = User.objects.get(username=options['user']) filter_args['user'] = user enrollments = CourseEnrollment.objects.filter(**filter_args) if options['noop']: print "Would have changed {num_enrollments} students from {from_mode} to {to_mode}".format( num_enrollments=enrollments.count(), from_mode=options['from_mode'], to_mode=options['to_mode'] ) else: for enrollment in enrollments: enrollment.update_enrollment(mode=options['to_mode']) enrollment.save()
def show_unit_extensions(request, course_id): """ Shows all of the students which have due date extensions for the given unit. """ course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) unit = find_unit(course, request.GET.get('url')) return JsonResponse(dump_module_extensions(course, unit))
def setUp(self): self.course_name = 'edX/toy/2012_Fall' # Create student account student = UserFactory.create() CourseEnrollmentFactory.create( user=student, course_id=SlashSeparatedCourseKey.from_deprecated_string( self.course_name)) self.client.login(username=student.username, password="******") try: # URL for dashboard self.url = reverse('dashboard') except NoReverseMatch: raise SkipTest( "Skip this test if url cannot be found (ie running from CMS tests)" ) # URL for email settings modal self.email_modal_link = (( '<a href="#email-settings-modal" class="email-settings" rel="leanModal" ' 'data-course-id="{0}/{1}/{2}" data-course-number="{1}" ' 'data-optout="False">Email Settings</a>').format( 'edX', 'toy', '2012_Fall'))
def get_student_progress_url(request, course_id): """ Get the progress url of a student. Limited to staff access. Takes query paremeter unique_student_identifier and if the student exists returns e.g. { 'progress_url': '/../...' } """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = get_student_from_identifier( request.GET.get('unique_student_identifier')) progress_url = reverse('student_progress', kwargs={ 'course_id': course_id.to_deprecated_string(), 'student_id': user.id }) response_payload = { 'course_id': course_id.to_deprecated_string(), 'progress_url': progress_url, } return JsonResponse(response_payload)
def course_about(request, course_id): """ Display the course's about page. Assumes the course_id is in a valid format. """ if microsite.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)): raise Http404 course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'see_exists', course_key) registered = registered_for_course(course, request.user) staff_access = has_access(request.user, 'staff', course) studio_url = get_studio_url(course_key, 'settings/details') if has_access(request.user, 'load', course): course_target = reverse('info', args=[course.id.to_deprecated_string()]) else: course_target = reverse('about_course', args=[course.id.to_deprecated_string()]) show_courseware_link = (has_access(request.user, 'load', course) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')) # Note: this is a flow for payment for course registration, not the Verified Certificate flow. registration_price = 0 in_cart = False reg_then_add_to_cart_link = "" if (settings.FEATURES.get('ENABLE_SHOPPING_CART') and settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')): registration_price = CourseMode.min_course_price_for_currency( course_key, settings.PAID_COURSE_REGISTRATION_CURRENCY[0]) if request.user.is_authenticated(): cart = shoppingcart.models.Order.get_cart_for_user(request.user) in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order( cart, course_key) reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format( reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string()) # see if we have already filled up all allowed enrollments is_course_full = CourseEnrollment.is_course_full(course) return render_to_response( 'courseware/course_about.html', { 'course': course, 'staff_access': staff_access, 'studio_url': studio_url, 'registered': registered, 'course_target': course_target, 'registration_price': registration_price, 'in_cart': in_cart, 'reg_then_add_to_cart_link': reg_then_add_to_cart_link, 'show_courseware_link': show_courseware_link, 'is_course_full': is_course_full })
def show_unit_extensions(request, course_id): """ Shows all of the students which have due date extensions for the given unit. """ course = get_course_by_id( SlashSeparatedCourseKey.from_deprecated_string(course_id)) unit = find_unit(course, request.GET.get('url')) return JsonResponse(dump_module_extensions(course, unit))
class MidcourseReverificationWindowFactory(DjangoModelFactory): """ Creates a generic MidcourseReverificationWindow. """ FACTORY_FOR = MidcourseReverificationWindow course_id = SlashSeparatedCourseKey.from_deprecated_string(u'MITx/999/Robot_Super_Course') # By default this factory creates a window that is currently open start_date = datetime.now(pytz.UTC) - timedelta(days=100) end_date = datetime.now(pytz.UTC) + timedelta(days=100)
def course_key_from_arg(self, arg): """ Convert the command line arg into a course key """ try: return CourseKey.from_string(arg) except InvalidKeyError: return SlashSeparatedCourseKey.from_deprecated_string(arg)