def mktg_course_about(request, course_id): """ This is the button that gets put into an iframe on the Drupal site """ try: course = get_course_with_access(request.user, course_id, 'see_exists') except (ValueError, Http404) as e: # if a course does not exist yet, display a coming # soon button return render_to_response( 'courseware/mktg_coming_soon.html', {'course_id': course_id} ) registered = registered_for_course(course, request.user) if has_access(request.user, course, 'load'): course_target = reverse('info', args=[course.id]) else: course_target = reverse('about_course', args=[course.id]) allow_registration = has_access(request.user, course, 'enroll') show_courseware_link = (has_access(request.user, course, 'load') or settings.FEATURES.get('ENABLE_LMS_MIGRATION')) course_modes = CourseMode.modes_for_course(course.id) return render_to_response('courseware/mktg_course_about.html', { 'course': course, 'registered': registered, 'allow_registration': allow_registration, 'course_target': course_target, 'show_courseware_link': show_courseware_link, 'course_modes': course_modes, })
def course_about(request, course_id): if settings.MITX_FEATURES.get("ENABLE_MKTG_SITE", False): raise Http404 course = get_course_with_access(request.user, course_id, "see_exists") registered = registered_for_course(course, request.user) if has_access(request.user, course, "load"): course_target = reverse("courseware", args=[course.id]) else: course_target = reverse("about_course", args=[course.id]) show_courseware_link = has_access(request.user, course, "load") or settings.MITX_FEATURES.get( "ENABLE_LMS_MIGRATION" ) return render_to_response( "courseware/course_about.html", { "course": course, "registered": registered, "course_target": course_target, "show_courseware_link": show_courseware_link, }, )
def test_course_access_to_beta_users(self): """ Test that beta testers can access `self_paced` course prior to start date. """ now = datetime.datetime.now(pytz.UTC) one_month_from_now = now + datetime.timedelta(days=30) course_options = { 'days_early_for_beta': 100, 'self_paced': True, 'start': one_month_from_now, } # Create a `self_paced` course and add a beta tester in it self_paced_course, self_paced_section = self.setup_course(**course_options) beta_tester = BetaTesterFactory(course_key=self_paced_course.id) # Verify course is `self_paced` and course has start date but not section. self.assertTrue(self_paced_course.self_paced) self.assertEqual(self_paced_course.start, one_month_from_now) self.assertIsNone(self_paced_section.start) # Verify that non-staff user do not have access to the course self.assertFalse(has_access(self.non_staff_user, 'load', self_paced_course)) # Verify beta tester can access the course as well as the course sections self.assertTrue(has_access(beta_tester, 'load', self_paced_course)) self.assertTrue(has_access(beta_tester, 'load', self_paced_section, self_paced_course.id))
def test_beta_period(self): """ Check that beta-test access works. """ student_email, student_password = self.ACCOUNT_INFO[0] instructor_email, instructor_password = self.ACCOUNT_INFO[1] # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) course_data = {'start': tomorrow} # self.course's hasn't started self.course = self.update_course(self.course, course_data) self.assertFalse(self.course.has_started()) # but should be accessible for beta testers self.course.lms.days_early_for_beta = 2 # student user shouldn't see it student_user = User.objects.get(email=student_email) self.assertFalse(has_access(student_user, self.course, 'load')) # now add the student to the beta test group group_name = course_beta_test_group_name(self.course.location) group = Group.objects.create(name=group_name) group.user_set.add(student_user) # now the student should see it self.assertTrue(has_access(student_user, self.course, 'load'))
def _get_user_certificate(request, user, course_key, course, preview_mode=None): """ Retrieves user's certificate from db. Creates one in case of preview mode. Returns None if there is no certificate generated for given user otherwise returns `GeneratedCertificate` instance. """ try: # Attempt to load the user's generated certificate data if preview_mode: user_certificate = GeneratedCertificate.objects.get( user=user, course_id=course_key, mode=preview_mode ) else: user_certificate = GeneratedCertificate.objects.get( user=user, course_id=course_key ) # If there's no generated certificate data for this user, we need to see if we're in 'preview' mode... # If we are, we'll need to create a mock version of the user_certificate container for previewing except GeneratedCertificate.DoesNotExist: if preview_mode and ( has_access(request.user, 'instructor', course) or has_access(request.user, 'staff', course)): user_certificate = GeneratedCertificate( mode=preview_mode, verify_uuid=unicode(uuid4().hex), modified_date=datetime.now().date() ) else: return None return user_certificate
def _create_base_discussion_view_context(request, course_key): """ Returns the default template context for rendering any discussion view. """ user = request.user cc_user = cc.User.from_django_user(user) user_info = cc_user.to_dict() course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, user) uses_bootstrap = USE_BOOTSTRAP_FLAG.is_enabled() return { 'csrf': csrf(request)['csrf_token'], 'course': course, 'user': user, 'user_info': user_info, 'staff_access': bool(has_access(user, 'staff', course)), 'roles': utils.get_role_ids(course_key), 'can_create_comment': has_permission(user, "create_comment", course.id), 'can_create_subcomment': has_permission(user, "create_sub_comment", course.id), 'can_create_thread': has_permission(user, "create_thread", course.id), 'flag_moderator': bool( has_permission(user, 'openclose_thread', course.id) or has_access(user, 'staff', course) ), 'course_settings': course_settings, 'disable_courseware_js': True, 'uses_bootstrap': uses_bootstrap, 'uses_pattern_library': not uses_bootstrap, }
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course = get_course_by_id(course_id, depth=None) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, course, 'instructor'), 'staff': has_access(request.user, course, 'staff'), 'forum_admin': has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ), } if not access['staff']: raise Http404() sections = [ _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id), _section_analytics(course_id), ] context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), 'sections': sections, } return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def test_student_has_access(self): """ Tests course student have right access to content w/o preview. """ course_key = self.course.id chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) overview = CourseOverview.get_from_id(course_key) # Enroll student to the course CourseEnrollmentFactory(user=self.student, course_id=self.course.id) modules = [ self.course, overview, chapter, ] with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = False for obj in modules: self.assertTrue(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))) with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = True for obj in modules: self.assertFalse(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id)))
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course = get_course_by_id(course_id, depth=None) access = { "admin": request.user.is_staff, "instructor": has_access(request.user, course, "instructor"), "staff": has_access(request.user, course, "staff"), "forum_admin": has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR), } if not access["staff"]: raise Http404() sections = [ _section_course_info(course_id), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id), _section_analytics(course_id), ] context = { "course": course, "old_dashboard_url": reverse("instructor_dashboard", kwargs={"course_id": course_id}), "sections": sections, } return render_to_response("instructor/instructor_dashboard_2/instructor_dashboard_2.html", context)
def _get_user_certificate(request, user, course_key, course, preview_mode=None): """ Retrieves user's certificate from db. Creates one in case of preview mode. Returns None if there is no certificate generated for given user otherwise returns `GeneratedCertificate` instance. """ user_certificate = None if preview_mode: # certificate is being previewed from studio if has_access(request.user, 'instructor', course) or has_access(request.user, 'staff', course): user_certificate = GeneratedCertificate( mode=preview_mode, verify_uuid=unicode(uuid4().hex), modified_date=datetime.now().date() ) else: # certificate is being viewed by learner or public try: user_certificate = GeneratedCertificate.eligible_certificates.get( user=user, course_id=course_key, status=CertificateStatuses.downloadable ) except GeneratedCertificate.DoesNotExist: pass return user_certificate
def mktg_course_about(request, course_id): """ This is the button that gets put into an iframe on the Drupal site """ try: course = get_course_with_access(request.user, course_id, "see_exists") except (ValueError, Http404) as e: # if a course does not exist yet, display a coming # soon button return render_to_response("courseware/mktg_coming_soon.html", {"course_id": course_id}) registered = registered_for_course(course, request.user) if has_access(request.user, course, "load"): course_target = reverse("info", args=[course.id]) else: course_target = reverse("about_course", args=[course.id]) allow_registration = has_access(request.user, course, "enroll") show_courseware_link = has_access(request.user, course, "load") or settings.FEATURES.get("ENABLE_LMS_MIGRATION") course_modes = CourseMode.modes_for_course(course.id) return render_to_response( "courseware/mktg_course_about.html", { "course": course, "registered": registered, "allow_registration": allow_registration, "course_target": course_target, "show_courseware_link": show_courseware_link, "course_modes": course_modes, }, )
def i_am_registered_for_the_course(coursenum, metadata, user='******'): # Create user if user == 'BetaTester': # Create the course now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=5) metadata.update({'days_early_for_beta': 5, 'start': tomorrow}) create_course_for_lti(coursenum, metadata) course_descriptor = world.scenario_dict['COURSE'] # create beta tester user = BetaTesterFactory(course_key=course_descriptor.id) normal_student = UserFactory() instructor = InstructorFactory(course_key=course_descriptor.id) assert not has_access(normal_student, 'load', course_descriptor) assert has_access(user, 'load', course_descriptor) assert has_access(instructor, 'load', course_descriptor) else: metadata.update({'start': datetime.datetime(1970, 1, 1, tzinfo=UTC)}) create_course_for_lti(coursenum, metadata) course_descriptor = world.scenario_dict['COURSE'] user = InstructorFactory(course_key=course_descriptor.id) # Enroll the user in the course and log them in if has_access(user, 'load', course_descriptor): world.enroll_user(user, course_descriptor.id) world.log_in(username=user.username, password='******')
def _do_test_beta_period(self): """Actually test beta periods, relying on settings to be right.""" # trust, but verify :) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) # Make courses start in the future tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1) # toy course's hasn't started self.toy.lms.start = tomorrow self.assertFalse(self.toy.has_started()) # but should be accessible for beta testers self.toy.lms.days_early_for_beta = 2 # student user shouldn't see it student_user = get_user(self.student) self.assertFalse(has_access(student_user, self.toy, 'load')) # now add the student to the beta test group group_name = course_beta_test_group_name(self.toy.location) group = Group.objects.create(name=group_name) group.user_set.add(student_user) # now the student should see it self.assertTrue(has_access(student_user, self.toy, 'load'))
def get_course_with_access(user, action, course_key, depth=0, check_if_enrolled=False): """ Given a course_key, look up the corresponding course descriptor, check that the user has the access to perform the specified action on the course, and return the descriptor. Raises a 404 if the course_key is invalid, or the user doesn't have access. depth: The number of levels of children for the modulestore to cache. None means infinite depth check_if_enrolled: If true, additionally verifies that the user is either enrolled in the course or has staff access. """ assert isinstance(course_key, CourseKey) course = get_course_by_id(course_key, depth=depth) access_response = has_access(user, action, course, course_key) if not access_response: # Deliberately return a non-specific error message to avoid # leaking info about access control settings raise CoursewareAccessException(access_response) if check_if_enrolled: # Verify that the user is either enrolled in the course or a staff member. # If user is not enrolled, raise UserNotEnrolled exception that will be caught by middleware. if not ((user.id and CourseEnrollment.is_enrolled(user, course_key)) or has_access(user, 'staff', course)): raise UserNotEnrolled(course_key) return course
def wrapper(request, course_id): """ Wraps the view function, performing access check, loading the course, and modifying the view's call signature. """ course_key = CourseKey.from_string(course_id) ccx = None if isinstance(course_key, CCXLocator): ccx_id = course_key.ccx ccx = CustomCourseForEdX.objects.get(pk=ccx_id) course_key = ccx.course_id course = get_course_by_id(course_key, depth=None) is_staff = has_access(request.user, 'staff', course) is_instructor = has_access(request.user, 'instructor', course) if is_staff or is_instructor: # if user is staff or instructor then he can view ccx coach dashboard. return view(request, course, ccx) else: role = CourseCcxCoachRole(course_key) if not role.has_user(request.user): return HttpResponseForbidden(_('You must be a CCX Coach to access this view.')) # if there is a ccx, we must validate that it is the ccx for this coach if ccx is not None: coach_ccx = get_ccx_by_ccx_id(course, request.user, ccx.id) if coach_ccx is None: return HttpResponseForbidden( _('You must be the coach for this ccx to access this view') ) return view(request, course, ccx)
def mktg_course_about(request, course_id): """This is the button that gets put into an iframe on the Drupal site.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: permission_name = microsite.get_value( 'COURSE_ABOUT_VISIBILITY_PERMISSION', settings.COURSE_ABOUT_VISIBILITY_PERMISSION ) course = get_course_with_access(request.user, permission_name, course_key) except (ValueError, Http404): # If a course does not exist yet, display a "Coming Soon" button return render_to_response( 'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()} ) registered = registered_for_course(course, request.user) 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()]) allow_registration = has_access(request.user, 'enroll', course) show_courseware_link = (has_access(request.user, 'load', course) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')) course_modes = CourseMode.modes_for_course_dict(course.id) context = { 'course': course, 'registered': registered, 'allow_registration': allow_registration, 'course_target': course_target, 'show_courseware_link': show_courseware_link, 'course_modes': course_modes, } if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): # Drupal will pass the organization's full name as a GET parameter. If no full name # is provided, the marketing iframe won't show the email opt-in checkbox. organization_full_name = request.GET.get('organization_full_name') context['organization_full_name'] = cgi.escape(organization_full_name) if organization_full_name else organization_full_name # The edx.org marketing site currently displays only in English. # To avoid displaying a different language in the register / access button, # we force the language to English. # However, OpenEdX installations with a different marketing front-end # may want to respect the language specified by the user or the site settings. force_english = settings.FEATURES.get('IS_EDX_DOMAIN', False) if force_english: translation.activate('en-us') try: return render_to_response('courseware/mktg_course_about.html', context) finally: # Just to be safe, reset the language if we forced it to be English. if force_english: translation.deactivate()
def user_can_access_course(self, user, course): """ Determines if the user is staff or an instructor for the course. Always returns True if DEBUG mode is enabled. """ return (settings.DEBUG or has_access(user, CourseStaffRole.ROLE, course) or has_access(user, CourseInstructorRole.ROLE, course))
def test_course_overview_unsupported_action(self): """ Check that calling has_access with an unsupported action raises a ValueError. """ overview = CourseOverview.get_from_id(self.course_default.id) with self.assertRaises(ValueError): access.has_access(self.user, "_non_existent_action", overview)
def test_content_beta_period(self): """ Check that beta-test access works for content. """ # student user shouldn't see it self.assertFalse(has_access(self.normal_student, self.content, 'load', self.course.id)) # now the student should see it self.assertTrue(has_access(self.beta_tester, self.content, 'load', self.course.id))
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course = get_course_by_id(course_id, depth=None) is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, course, 'instructor'), 'staff': has_access(request.user, course, 'staff'), 'forum_admin': has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ), } if not access['staff']: raise Http404() sections = [ _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id, access), _section_analytics(course_id, access), ] if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \ is_studio_course and CourseAuthorization.instructor_email_enabled(course_id): sections.append(_section_send_email(course_id, access, course)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course_id, access)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]['enrollment_count'] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, } return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
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 = get_course_with_access(request.user, course_id, "see_exists") registered = registered_for_course(course, request.user) staff_access = has_access(request.user, course, "staff") studio_url = get_studio_url(course_id, "settings/details") if has_access(request.user, course, "load"): course_target = reverse("info", args=[course.id]) else: course_target = reverse("about_course", args=[course.id]) show_courseware_link = has_access(request.user, course, "load") 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_id, 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_id) 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 ) # 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 dashboard(request): user = request.user enrollments = CourseEnrollment.objects.filter(user=user) # Build our courses list for the user, but ignore any courses that no longer # exist (because the course IDs have changed). Still, we don't delete those # enrollments, because it could have been a data push snafu. courses = [] for enrollment in enrollments: try: courses.append(course_from_id(enrollment.course_id)) except ItemNotFoundError: log.error("User {0} enrolled in non-existent course {1}" .format(user.username, enrollment.course_id)) message = "" if not user.is_active: message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) # Global staff can see what courses errored on their dashboard staff_access = False errored_courses = {} if has_access(user, 'global', 'staff'): # Show any courses that errored on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = frozenset(course.id for course in courses if has_access(request.user, course, 'load')) cert_statuses = {course.id: cert_info(request.user, course) for course in courses} exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses} # Get the 3 most recent news top_news = _get_news(top=3) if not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False) else None # get info w.r.t ExternalAuthMap external_auth_map = None try: external_auth_map = ExternalAuthMap.objects.get(user=user) except ExternalAuthMap.DoesNotExist: pass context = {'courses': courses, 'message': message, 'external_auth_map': external_auth_map, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'cert_statuses': cert_statuses, 'news': top_news, 'exam_registrations': exam_registrations, } return render_to_response('dashboard.html', context)
def update_forum_role_membership(request, course_id): """ Modify user's forum role. The requesting user must be at least staff. Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR which is limited to instructors. No one can revoke an instructors FORUM_ROLE_ADMINISTRATOR status. Query parameters: - `email` is the target users email - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] - `action` is one of ['allow', 'revoke'] """ course = get_course_by_id(course_id) has_instructor_access = has_access(request.user, course, 'instructor') has_forum_admin = has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ) email = request.GET.get('email') rolename = request.GET.get('rolename') action = request.GET.get('action') # default roles require either (staff & forum admin) or (instructor) if not (has_forum_admin or has_instructor_access): return HttpResponseBadRequest( "Operation requires staff & forum admin or instructor access" ) # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor) if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access: return HttpResponseBadRequest("Operation requires instructor access.") if not rolename in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]: return HttpResponseBadRequest("Unrecognized rolename '{}'.".format(rolename)) user = User.objects.get(email=email) target_is_instructor = has_access(user, course, 'instructor') # cannot revoke instructor if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: return HttpResponseBadRequest("Cannot revoke instructor forum admin privelages.") try: access.update_forum_role_membership(course_id, user, rolename, action) except Role.DoesNotExist: return HttpResponseBadRequest("Role does not exist.") response_payload = { 'course_id': course_id, 'action': action, } response = HttpResponse( json.dumps(response_payload), content_type="application/json" ) return response
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 update_forum_role_membership(request, course_id): """ Modify user's forum role. The requesting user must be at least staff. Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR which is limited to instructors. No one can revoke an instructors FORUM_ROLE_ADMINISTRATOR status. Query parameters: - `email` is the target users email - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] - `action` is one of ['allow', 'revoke'] """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_id) has_instructor_access = has_access(request.user, 'instructor', course) has_forum_admin = has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ) unique_student_identifier = request.GET.get('unique_student_identifier') rolename = request.GET.get('rolename') action = request.GET.get('action') # default roles require either (staff & forum admin) or (instructor) if not (has_forum_admin or has_instructor_access): return HttpResponseBadRequest( "Operation requires staff & forum admin or instructor access" ) # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor) if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access: return HttpResponseBadRequest("Operation requires instructor access.") if not rolename in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]: return HttpResponseBadRequest(strip_tags( "Unrecognized rolename '{}'.".format(rolename) )) user = get_student_from_identifier(unique_student_identifier) target_is_instructor = has_access(user, 'instructor', course) # cannot revoke instructor if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: return HttpResponseBadRequest("Cannot revoke instructor forum admin privileges.") try: update_forum_role(course_id, user, rolename, action) except Role.DoesNotExist: return HttpResponseBadRequest("Role does not exist.") response_payload = { 'course_id': course_id.to_deprecated_string(), 'action': action, } return JsonResponse(response_payload)
def check_course_access(course, user, action, check_if_enrolled=False, check_survey_complete=True): """ Check that the user has the access to perform the specified action on the course (CourseDescriptor|CourseOverview). check_if_enrolled: If true, additionally verifies that the user is enrolled. check_survey_complete: If true, additionally verifies that the user has completed the survey. """ # Allow staff full access to the course even if not enrolled if has_access(user, 'staff', course.id): return request = get_current_request() check_content_start_date_for_masquerade_user(course.id, user, request, course.start) access_response = has_access(user, action, course, course.id) if not access_response: # Redirect if StartDateError if isinstance(access_response, StartDateError): start_date = strftime_localized(course.start, 'SHORT_DATE') params = QueryDict(mutable=True) params['notlive'] = start_date raise CourseAccessRedirect('{dashboard_url}?{params}'.format( dashboard_url=reverse('dashboard'), params=params.urlencode() ), access_response) # Redirect if AuditExpiredError if isinstance(access_response, AuditExpiredError): params = QueryDict(mutable=True) params['access_response_error'] = access_response.additional_context_user_message raise CourseAccessRedirect('{dashboard_url}?{params}'.format( dashboard_url=reverse('dashboard'), params=params.urlencode() ), access_response) # Redirect if the user must answer a survey before entering the course. if isinstance(access_response, MilestoneAccessError): raise CourseAccessRedirect('{dashboard_url}'.format( dashboard_url=reverse('dashboard'), ), access_response) # Deliberately return a non-specific error message to avoid # leaking info about access control settings raise CoursewareAccessException(access_response) if check_if_enrolled: # If the user is not enrolled, redirect them to the about page if not CourseEnrollment.is_enrolled(user, course.id): raise CourseAccessRedirect(reverse('about_course', args=[unicode(course.id)])) # Redirect if the user must answer a survey before entering the course. if check_survey_complete and action == 'load': if is_survey_required_and_unanswered(user, course): raise CourseAccessRedirect(reverse('course_survey', args=[unicode(course.id)]))
def instructor_dashboard_2(request, course_id): """Display the instructor dashboard for a course.""" course = get_course_by_id(course_id, depth=None) is_studio_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE access = { "admin": request.user.is_staff, "instructor": has_access(request.user, course, "instructor"), "staff": has_access(request.user, course, "staff"), "forum_admin": has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR), } if not access["staff"]: raise Http404() sections = [ _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id, access), _section_analytics(course_id, access), ] if settings.FEATURES.get("INDIVIDUAL_DUE_DATES") and access["instructor"]: sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if ( settings.FEATURES["ENABLE_INSTRUCTOR_EMAIL"] and is_studio_course and CourseAuthorization.instructor_email_enabled(course_id) ): sections.append(_section_send_email(course_id, access, course)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]["enrollment_count"] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { "course": course, "old_dashboard_url": reverse("instructor_dashboard", kwargs={"course_id": course_id}), "studio_url": studio_url, "sections": sections, "disable_buttons": disable_buttons, } return render_to_response("instructor/instructor_dashboard_2/instructor_dashboard_2.html", context)
def test_course_beta_period(self): """ Check that beta-test access works for courses. """ self.assertFalse(self.course.has_started()) # student user shouldn't see it self.assertFalse(has_access(self.normal_student, self.course, 'load')) # now the student should see it self.assertTrue(has_access(self.beta_tester, self.course, 'load'))
def test_has_access_with_preview_mode(self): """ Tests particular user's can access content via has_access in preview mode. """ self.assertTrue(bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id))) self.assertTrue(bool(access.has_access(self.course_staff, 'staff', self.course, course_key=self.course.id))) self.assertTrue(bool(access.has_access( self.course_instructor, 'staff', self.course, course_key=self.course.id ))) self.assertFalse(bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id))) self.assertFalse(bool(access.has_access(self.student, 'load', self.course, course_key=self.course.id)))
def mktg_course_about(request, course_id): """ This is the button that gets put into an iframe on the Drupal site """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: course = get_course_with_access(request.user, 'see_exists', course_key) except (ValueError, Http404) as e: # if a course does not exist yet, display a coming # soon button return render_to_response( 'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()} ) registered = registered_for_course(course, request.user) 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()]) allow_registration = has_access(request.user, 'enroll', course) show_courseware_link = (has_access(request.user, 'load', course) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')) course_modes = CourseMode.modes_for_course_dict(course.id) context = { 'course': course, 'registered': registered, 'allow_registration': allow_registration, 'course_target': course_target, 'show_courseware_link': show_courseware_link, 'course_modes': course_modes, } # The edx.org marketing site currently displays only in English. # To avoid displaying a different language in the register / access button, # we force the language to English. # However, OpenEdX installations with a different marketing front-end # may want to respect the language specified by the user or the site settings. force_english = settings.FEATURES.get('IS_EDX_DOMAIN', False) if force_english: translation.activate('en-us') try: return render_to_response('courseware/mktg_course_about.html', context) finally: # Just to be safe, reset the language if we forced it to be English. if force_english: translation.deactivate()
def forum_form_discussion(request, course_id): """ Renders the main Discussion page, potentially filtered by a search query """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, course_id, 'load_forum') with newrelic.agent.FunctionTrace(nr_transaction, "get_discussion_category_map"): category_map = utils.get_discussion_category_map(course) try: unsafethreads, query_params = get_threads( request, course_id) # This might process a search query threads = [utils.safe_content(thread) for thread in unsafethreads] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response('discussion/maintenance.html', {}) user = cc.User.from_django_user(request.user) user_info = user.to_dict() with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_id, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, 'num_pages': query_params['num_pages'], 'page': query_params['page'], }) else: #recent_active_threads = cc.search_recent_active_threads( # course_id, # recursive=False, # query_params={'follower_id': request.user.id}, #) #trending_tags = cc.search_trending_tags( # course_id, #) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): cohorts = get_course_cohorts(course_id) cohorted_commentables = get_cohorted_commentables(course_id) user_cohort_id = get_cohort_id(request.user, course_id) context = { 'csrf': csrf(request)['csrf_token'], 'course': course, #'recent_active_threads': recent_active_threads, #'trending_tags': trending_tags, 'staff_access': has_access(request.user, course, 'staff'), 'threads': saxutils.escape(json.dumps(threads), escapedict), 'thread_pages': query_params['num_pages'], 'user_info': saxutils.escape(json.dumps(user_info), escapedict), 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), 'course_id': course.id, 'category_map': category_map, 'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict), 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id), 'cohorts': cohorts, 'user_cohort': user_cohort_id, 'cohorted_commentables': cohorted_commentables, 'is_course_cohorted': is_course_cohorted(course_id) } # print "start rendering.." return render_to_response('discussion/index.html', context)
def test_has_access_in_preview_mode_with_group(self): """ Test that a user masquerading as a member of a group sees appropriate content in preview mode. """ # Note about UserPartition and UserPartition Group IDs: these must not conflict with IDs used # by dynamic user partitions. partition_id = MINIMUM_STATIC_PARTITION_ID group_0_id = MINIMUM_STATIC_PARTITION_ID + 1 group_1_id = MINIMUM_STATIC_PARTITION_ID + 2 user_partition = UserPartition( partition_id, 'Test User Partition', '', [Group(group_0_id, 'Group 1'), Group(group_1_id, 'Group 2')], scheme_id='cohort') self.course.user_partitions.append(user_partition) self.course.cohort_config = {'cohorted': True} chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) chapter.group_access = {partition_id: [group_0_id]} modulestore().update_item(self.course, ModuleStoreEnum.UserID.test) # User should not be able to preview when masquerading as student (and not in the group above). with patch('courseware.access.get_user_role') as mock_user_role: mock_user_role.return_value = 'student' self.assertFalse( bool( access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))) # Should be able to preview when in staff or instructor role. for mocked_role in ['staff', 'instructor']: with patch('courseware.access.get_user_role') as mock_user_role: mock_user_role.return_value = mocked_role self.assertTrue( bool( access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))) # Now install masquerade group and set staff as a member of that. self.assertEqual( 200, masquerade_as_group_member(self.global_staff, self.course, partition_id, group_0_id)) # Can load the chapter since user is in the group. self.assertTrue( bool( access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))) # Move the user to be a part of the second group. self.assertEqual( 200, masquerade_as_group_member(self.global_staff, self.course, partition_id, group_1_id)) # Cannot load the chapter since user is in a different group. self.assertFalse( bool( access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id)))
def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) user = cc.User.from_django_user(request.user) user_info = user.to_dict() try: unsafethreads, query_params = get_threads( request, course) # This might process a search query is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads ] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response('discussion/maintenance.html', {}) except ValueError: return HttpResponseBadRequest("Invalid group_id") with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, 'num_pages': query_params['num_pages'], 'page': query_params['page'], 'corrected_text': query_params['corrected_text'], }) else: with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort_id = get_cohort_id(request.user, course_key) context = { 'csrf': csrf(request)['csrf_token'], 'course': course, #'recent_active_threads': recent_active_threads, 'staff_access': bool(has_access(request.user, 'staff', course)), 'threads': _attr_safe_json(threads), 'thread_pages': query_params['num_pages'], 'user_info': _attr_safe_json(user_info), 'can_create_comment': _attr_safe_json( has_permission(request.user, "create_comment", course.id)), 'can_create_subcomment': _attr_safe_json( has_permission(request.user, "create_sub_comment", course.id)), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course)), 'annotated_content_info': _attr_safe_json(annotated_content_info), 'course_id': course.id.to_deprecated_string(), 'roles': _attr_safe_json(utils.get_role_ids(course_key)), 'is_moderator': has_permission(request.user, "see_all_cohorts", course_key), 'cohorts': course_settings[ "cohorts"], # still needed to render _thread_list_template 'user_cohort': user_cohort_id, # read from container in NewPostView 'is_course_cohorted': is_course_cohorted( course_key), # still needed to render _thread_list_template 'sort_preference': user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': _attr_safe_json(course_settings) } # print "start rendering.." return render_to_response('discussion/index.html', context)
def index(request, course_id, chapter=None, section=None, position=None): """ Displays courseware accordion and associated content. If course, chapter, and section are all specified, renders the page, or returns an error if they are invalid. If section is not specified, displays the accordion opened to the right chapter. If neither chapter or section are specified, redirects to user's most recent chapter, or the first chapter if this is the user's first visit. Arguments: - request : HTTP request - course_id : course id (str: ORG/course/URL_NAME) - chapter : chapter url_name (str) - section : section url_name (str) - position : position in module, eg of <sequential> module (str) Returns: - HTTPresponse """ user = User.objects.prefetch_related("groups").get(id=request.user.id) request.user = user # keep just one instance of User course = get_course_with_access(user, course_id, 'load', depth=2) staff_access = has_access(user, course, 'staff') registered = registered_for_course(course, user) if not registered: # TODO (vshnayder): do course instructors need to be registered to see course? log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url())) return redirect(reverse('about_course', args=[course.id])) masq = setup_masquerade(request, staff_access) try: field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, user, course, depth=2) course_module = get_module_for_descriptor(user, request, course, field_data_cache, course.id) if course_module is None: log.warning( 'If you see this, something went wrong: if we got this' ' far, should have gotten a course module for this user') return redirect(reverse('about_course', args=[course.id])) if chapter is None: return redirect_to_course_position(course_module) context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section, field_data_cache), 'COURSE_TITLE': course.display_name_with_default, 'course': course, 'init': '', 'content': '', 'staff_access': staff_access, 'masquerade': masq, 'xqa_server': settings.MITX_FEATURES.get( 'USE_XQA_SERVER', 'http://*****:*****@content-qa.mitx.mit.edu/xqa') } # Only show the chat if it's enabled by the course and in the # settings. show_chat = course.show_chat and settings.MITX_FEATURES['ENABLE_CHAT'] if show_chat: context['chat'] = chat_settings(course, user) # If we couldn't load the chat settings, then don't show # the widget in the courseware. if context['chat'] is None: show_chat = False context['show_chat'] = show_chat chapter_descriptor = course.get_child_by( lambda m: m.url_name == chapter) if chapter_descriptor is not None: save_child_position(course_module, chapter) else: raise Http404( 'No chapter descriptor found with name {}'.format(chapter)) chapter_module = course_module.get_child_by( lambda m: m.url_name == chapter) if chapter_module is None: # User may be trying to access a chapter that isn't live yet if masq == 'student': # if staff is masquerading as student be kinder, don't 404 log.debug('staff masq as student: no chapter %s' % chapter) return redirect(reverse('courseware', args=[course.id])) raise Http404 if section is not None: section_descriptor = chapter_descriptor.get_child_by( lambda m: m.url_name == section) if section_descriptor is None: # Specifically asked-for section doesn't exist if masq == 'student': # if staff is masquerading as student be kinder, don't 404 log.debug('staff masq as student: no section %s' % section) return redirect(reverse('courseware', args=[course.id])) raise Http404 # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None # which will prefetch the children more efficiently than doing a recursive load section_descriptor = modulestore().get_instance( course.id, section_descriptor.location, depth=None) # Load all descendants of the section, because we're going to display its # html, which in general will need all of its children section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, user, section_descriptor, depth=None) section_module = get_module_for_descriptor( request.user, request, section_descriptor, section_field_data_cache, course_id, position) if section_module is None: # User may be trying to be clever and access something # they don't have access to. raise Http404 # Save where we are in the chapter save_child_position(chapter_module, section) # check here if this section *is* a timed module. if section_module.category == 'timelimit': timer_context = update_timelimit_module( user, course_id, student_module_cache, section_descriptor, section_module) if 'timer_expiration_duration' in timer_context: context.update(timer_context) else: # if there is no expiration defined, then we know the timer has expired: return HttpResponseRedirect( timer_context['time_expired_redirect_url']) else: # check here if this page is within a course that has an active timed module running. If so, then # add in the appropriate timer information to the rendering context: context.update( check_for_active_timelimit_module(request, course_id, course)) context['content'] = section_module.runtime.render( section_module, None, 'student_view').content else: # section is none, so display a message prev_section = get_current_child(chapter_module) if prev_section is None: # Something went wrong -- perhaps this chapter has no sections visible to the user raise Http404 prev_section_url = reverse('courseware_section', kwargs={ 'course_id': course_id, 'chapter': chapter_descriptor.url_name, 'section': prev_section.url_name }) context['content'] = render_to_string( 'courseware/welcome-back.html', { 'course': course, 'chapter_module': chapter_module, 'prev_section': prev_section, 'prev_section_url': prev_section_url }) result = render_to_response('courseware/courseware.html', context) except Exception as e: if isinstance(e, Http404): # let it propagate raise # In production, don't want to let a 500 out for any reason if settings.DEBUG: raise else: log.exception("Error in index view: user={user}, course={course}," " chapter={chapter} section={section}" "position={position}".format(user=user, course=course, chapter=chapter, section=section, position=position)) try: result = render_to_response('courseware/courseware-error.html', { 'staff_access': staff_access, 'course': course }) except: # Let the exception propagate, relying on global config to at # at least return a nice error message log.exception("Error while rendering courseware-error page") raise return result
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: log.error( u"Unable to find course with course key %s while loading the Instructor Dashboard.", course_id) return HttpResponseServerError() course = get_course_by_id(course_key, depth=0) access = { 'admin': request.user.is_staff, 'instructor': bool(has_access(request.user, 'instructor', course)), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user), 'staff': bool(has_access(request.user, 'staff', course)), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), } if not access['staff']: raise Http404() is_white_label = CourseMode.is_white_label(course_key) sections = [ _section_course_info(course, access), _section_membership(course, access, is_white_label), _section_cohort_management(course, access), _section_student_admin(course, access), _section_data_download(course, access), ] analytics_dashboard_message = None if settings.ANALYTICS_DASHBOARD_URL: # Construct a URL to the external analytics dashboard analytics_dashboard_url = '{0}/courses/{1}'.format( settings.ANALYTICS_DASHBOARD_URL, unicode(course_key)) link_start = "<a href=\"{}\" target=\"_blank\">".format( analytics_dashboard_url) analytics_dashboard_message = _( "To gain insights into student enrollment and participation {link_start}" "visit {analytics_dashboard_name}, our new course analytics product{link_end}." ) analytics_dashboard_message = analytics_dashboard_message.format( link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME) # Temporarily show the "Analytics" section until we have a better way of linking to Insights sections.append(_section_analytics(course, access)) # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_mode_has_price = False paid_modes = CourseMode.paid_modes_for_course(course_key) if len(paid_modes) == 1: course_mode_has_price = True elif len(paid_modes) > 1: log.error( u"Course %s has %s course modes with payment options. Course must only have " u"one paid course mode to enable eCommerce options.", unicode(course_key), len(paid_modes)) if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']: sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course, access)) # Gate access to Ecommerce tab if course_mode_has_price and (access['finance_admin'] or access['sales_admin']): sections.append( _section_e_commerce(course, access, paid_modes[0], is_white_label, is_white_label)) # Gate access to Proctoring tab if settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) and course.enable_proctored_exams: sections.append(_section_proctoring(course, access)) # Certificates panel # This is used to generate example certificates # and enable self-generated certificates for a course. certs_enabled = CertificateGenerationConfiguration.current().enabled if certs_enabled and access['admin']: sections.append(_section_certificates(course)) disable_buttons = not _is_small_course(course_key) context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}), 'studio_url': get_studio_url(course, 'course'), 'sections': sections, 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message } return render_to_response( 'instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'honor': # The user will have already been enrolled in the honor mode at this # point, so we just redirect them to the dashboard, thereby avoiding # hitting the database a second time attempting to enroll them. return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}))
def check_course_access(course, user, action, check_if_enrolled=False, check_survey_complete=True): """ Check that the user has the access to perform the specified action on the course (CourseDescriptor|CourseOverview). check_if_enrolled: If true, additionally verifies that the user is enrolled. check_survey_complete: If true, additionally verifies that the user has completed the survey. """ # Allow staff full access to the course even if not enrolled if has_access(user, 'staff', course.id): return request = get_current_request() check_content_start_date_for_masquerade_user(course.id, user, request, course.start) access_response = has_access(user, action, course, course.id) if not access_response: # Redirect if StartDateError if isinstance(access_response, StartDateError): start_date = strftime_localized(course.start, 'SHORT_DATE') params = QueryDict(mutable=True) params['notlive'] = start_date raise CourseAccessRedirect( '{dashboard_url}?{params}'.format( dashboard_url=reverse('dashboard'), params=params.urlencode()), access_response) # Redirect if AuditExpiredError if isinstance(access_response, AuditExpiredError): params = QueryDict(mutable=True) params[ 'access_response_error'] = access_response.additional_context_user_message raise CourseAccessRedirect( '{dashboard_url}?{params}'.format( dashboard_url=reverse('dashboard'), params=params.urlencode()), access_response) # Redirect if the user must answer a survey before entering the course. if isinstance(access_response, MilestoneAccessError): raise CourseAccessRedirect( '{dashboard_url}'.format(dashboard_url=reverse('dashboard'), ), access_response) # Deliberately return a non-specific error message to avoid # leaking info about access control settings raise CoursewareAccessException(access_response) if check_if_enrolled: # If the user is not enrolled, redirect them to the about page if not CourseEnrollment.is_enrolled(user, course.id): raise CourseAccessRedirect( reverse('about_course', args=[six.text_type(course.id)])) # Redirect if the user must answer a survey before entering the course. if check_survey_complete and action == 'load': if is_survey_required_and_unanswered(user, course): raise CourseAccessRedirect( reverse('course_survey', args=[six.text_type(course.id)]))
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: log.error( u"Unable to find course with course key %s while loading the Instructor Dashboard.", course_id) return HttpResponseServerError() course = get_course_by_id(course_key, depth=0) access = { 'admin': request.user.is_staff, 'instructor': bool(has_access(request.user, 'instructor', course)), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user), 'staff': bool(has_access(request.user, 'staff', course)), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), } if not access['staff']: raise Http404() is_white_label = CourseMode.is_white_label(course_key) reports_enabled = configuration_helpers.get_value('SHOW_ECOMMERCE_REPORTS', False) sections = [ _section_course_info(course, access), _section_membership(course, access), _section_cohort_management(course, access), _section_discussions_management(course, access), _section_student_admin(course, access), _section_data_download(course, access), ] analytics_dashboard_message = None if show_analytics_dashboard_message(course_key): # Construct a URL to the external analytics dashboard analytics_dashboard_url = '{0}/courses/{1}'.format( settings.ANALYTICS_DASHBOARD_URL, six.text_type(course_key)) link_start = HTML(u"<a href=\"{}\" target=\"_blank\">").format( analytics_dashboard_url) analytics_dashboard_message = _( u"To gain insights into student enrollment and participation {link_start}" u"visit {analytics_dashboard_name}, our new course analytics product{link_end}." ) analytics_dashboard_message = Text(analytics_dashboard_message).format( link_start=link_start, link_end=HTML("</a>"), analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME) # Temporarily show the "Analytics" section until we have a better way of linking to Insights sections.append(_section_analytics(course, access)) # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_mode_has_price = False paid_modes = CourseMode.paid_modes_for_course(course_key) if len(paid_modes) == 1: course_mode_has_price = True elif len(paid_modes) > 1: log.error( u"Course %s has %s course modes with payment options. Course must only have " u"one paid course mode to enable eCommerce options.", six.text_type(course_key), len(paid_modes)) if access['instructor'] and is_enabled_for_course(course_key): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if is_bulk_email_feature_enabled(course_key): sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course, access)) # Gate access to Ecommerce tab if course_mode_has_price and (access['finance_admin'] or access['sales_admin']): sections.append( _section_e_commerce(course, access, paid_modes[0], is_white_label, reports_enabled)) # Gate access to Special Exam tab depending if either timed exams or proctored exams # are enabled in the course user_has_access = any([ request.user.is_staff, CourseStaffRole(course_key).has_user(request.user), CourseInstructorRole(course_key).has_user(request.user) ]) course_has_special_exams = course.enable_proctored_exams or course.enable_timed_exams can_see_special_exams = course_has_special_exams and user_has_access and settings.FEATURES.get( 'ENABLE_SPECIAL_EXAMS', False) if can_see_special_exams: sections.append(_section_special_exams(course, access)) # Certificates panel # This is used to generate example certificates # and enable self-generated certificates for a course. # Note: This is hidden for all CCXs certs_enabled = CertificateGenerationConfiguration.current( ).enabled and not hasattr(course_key, 'ccx') if certs_enabled and access['admin']: sections.append(_section_certificates(course)) openassessment_blocks = modulestore().get_items( course_key, qualifiers={'category': 'openassessment'}) # filter out orphaned openassessment blocks openassessment_blocks = [ block for block in openassessment_blocks if block.parent is not None ] if len(openassessment_blocks) > 0: sections.append( _section_open_response_assessment(request, course, openassessment_blocks, access)) disable_buttons = not _is_small_course(course_key) certificate_white_list = CertificateWhitelist.get_certificate_white_list( course_key) generate_certificate_exceptions_url = reverse( 'generate_certificate_exceptions', kwargs={ 'course_id': six.text_type(course_key), 'generate_for': '' }) generate_bulk_certificate_exceptions_url = reverse( 'generate_bulk_certificate_exceptions', kwargs={'course_id': six.text_type(course_key)}) certificate_exception_view_url = reverse( 'certificate_exception_view', kwargs={'course_id': six.text_type(course_key)}) certificate_invalidation_view_url = reverse( 'certificate_invalidation_view', kwargs={'course_id': six.text_type(course_key)}) certificate_invalidations = CertificateInvalidation.get_certificate_invalidations( course_key) context = { 'course': course, 'studio_url': get_studio_url(course, 'course'), 'sections': sections, 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message, 'certificate_white_list': certificate_white_list, 'certificate_invalidations': certificate_invalidations, 'generate_certificate_exceptions_url': generate_certificate_exceptions_url, 'generate_bulk_certificate_exceptions_url': generate_bulk_certificate_exceptions_url, 'certificate_exception_view_url': certificate_exception_view_url, 'certificate_invalidation_view_url': certificate_invalidation_view_url, } return render_to_response( 'instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def is_enabled(cls, course, user=None): """ Returns true if the specified user has staff access. """ return bool(user and has_access(user, 'staff', course, course.id))
def to_representation(self, course_overview): course_id = unicode(course_overview.id) request = self.context.get('request') return { # identifiers 'id': course_id, 'name': course_overview.display_name, 'number': course_overview.display_number_with_default, 'org': course_overview.display_org_with_default, # dates 'start': course_overview.start, 'start_display': course_overview.start_display, 'start_type': course_overview.start_type, 'end': course_overview.end, # notification info 'subscription_id': course_overview.clean_id(padding_char='_'), # access info 'courseware_access': has_access(request.user, 'load_mobile', course_overview).to_json(), # various URLs # course_image is sent in both new and old formats # (within media to be compatible with the new Course API) 'media': { 'course_image': { 'uri': course_overview.course_image_url, 'name': 'Course Image', } }, 'course_image': course_overview.course_image_url, 'course_about': get_link_for_about_page(course_overview), 'course_sharing_utm_parameters': get_encoded_course_sharing_utm_params(), 'course_updates': reverse( 'course-updates-list', kwargs={'course_id': course_id}, request=request, ), 'course_handouts': reverse( 'course-handouts-list', kwargs={'course_id': course_id}, request=request, ), 'discussion_url': reverse( 'discussion_course', kwargs={'course_id': course_id}, request=request, ) if course_overview.is_discussion_tab_enabled() else None, 'video_outline': reverse( 'video-summary-list', kwargs={'course_id': course_id}, request=request, ), }
def get_module_for_descriptor_internal( user, descriptor, student_data, course_id, # pylint: disable=invalid-name track_function, xqueue_callback_url_prefix, request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path='', user_location=None, disable_staff_debug_info=False, course=None): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. Arguments: request_token (str): A unique token for this request, used to isolate xblock rendering """ (system, student_data) = get_module_system_for_user( user=user, student_data= student_data, # These have implicit user bindings, the rest of args are considered not to descriptor=descriptor, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, disable_staff_debug_info=disable_staff_debug_info, course=course) descriptor.bind_for_student( system, user.id, [ partial(OverrideFieldData.wrap, user, course), partial(LmsFieldData, student_data=student_data), ], ) descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access # Do not check access when it's a noauth request. # Not that the access check needs to happen after the descriptor is bound # for the student, since there may be field override data for the student # that affects xblock visibility. if getattr(user, 'known', True): if not has_access(user, 'load', descriptor, course_id): return None return descriptor
def combined_notifications(course, user): """ Show notifications to a given user for a given course. Get notifications from the cache if possible, or from the grading controller server if not. @param course: The course object for which we are getting notifications @param user: The user object for which we are getting notifications @return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification image), and response (actual response from grading controller server). """ #Set up return values so that we can return them for error cases pending_grading = False img_path = "" notifications = {} notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } #We don't want to show anonymous users anything. if not user.is_authenticated(): return notification_dict #Define a mock modulesystem system = ModuleSystem(ajax_url=None, track_function=None, get_module=None, render_template=render_to_string, replace_urls=None, xblock_model_data={}) #Initialize controller query service using our mock system controller_qs = ControllerQueryService( settings.OPEN_ENDED_GRADING_INTERFACE, system) student_id = unique_id_for_user(user) user_is_staff = has_access(user, course, 'staff') course_id = course.id notification_type = "combined" #See if we have a stored value in the cache success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict #Get the time of the last login of the user last_login = user.last_login last_time_viewed = last_login - datetime.timedelta( seconds=(NOTIFICATION_CACHE_TIME + 60)) try: #Get the notifications from the grading controller controller_response = controller_qs.check_combined_notifications( course.id, student_id, user_is_staff, last_time_viewed) notifications = json.loads(controller_response) if notifications['success']: if notifications['staff_needs_to_grade'] or notifications[ 'student_needs_to_peer_grade']: pending_grading = True except: #Non catastrophic error, so no real action #This is a dev_facing_error log.exception( "Problem with getting notifications from controller query service for course {0} user {1}." .format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } #Store the notifications in the cache set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def get_module_system_for_user( user, student_data, # TODO # pylint: disable=too-many-statements # Arguments preceding this comment have user binding, those following don't descriptor, course_id, track_function, xqueue_callback_url_prefix, request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path='', user_location=None, disable_staff_debug_info=False, course=None): """ Helper function that returns a module system and student_data bound to a user and a descriptor. The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module, to allow an existing module to be re-bound to a user. Most of the user bindings happen when creating the closures that feed the instantiation of ModuleSystem. The arguments fall into two categories: those that have explicit or implicit user binding, which are user and student_data, and those don't and are just present so that ModuleSystem can be instantiated, which are all the other arguments. Ultimately, this isn't too different than how get_module_for_descriptor_internal was before refactoring. Arguments: see arguments for get_module() request_token (str): A token unique to the request use by xblock initialization Returns: (LmsModuleSystem, KvsFieldData): (module system, student_data) bound to, primarily, the user and descriptor """ def make_xqueue_callback(dispatch='score_update'): """ Returns fully qualified callback URL for external queueing system """ relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id.to_deprecated_string(), userid=str(user.id), mod_id=descriptor.location.to_deprecated_string(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': XQUEUE_INTERFACE, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user=user, descriptor=descriptor, student_data=student_data, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course) def _fulfill_content_milestones(user, course_key, content_key): """ Internal helper to handle milestone fulfillments for the specified content module """ # Fulfillment Use Case: Entrance Exam # If this module is part of an entrance exam, we'll need to see if the student # has reached the point at which they can collect the associated milestone if settings.FEATURES.get('ENTRANCE_EXAMS', False): course = modulestore().get_course(course_key) content = modulestore().get_item(content_key) entrance_exam_enabled = getattr(course, 'entrance_exam_enabled', False) in_entrance_exam = getattr(content, 'in_entrance_exam', False) if entrance_exam_enabled and in_entrance_exam: # We don't have access to the true request object in this context, but we can use a mock request = RequestFactory().request() request.user = user exam_pct = get_entrance_exam_score(request, course) if exam_pct >= course.entrance_exam_minimum_score_pct: exam_key = UsageKey.from_string(course.entrance_exam_id) relationship_types = milestones_helpers.get_milestone_relationship_types( ) content_milestones = milestones_helpers.get_course_content_milestones( course_key, exam_key, relationship=relationship_types['FULFILLS']) # Add each milestone to the user's set... user = {'id': request.user.id} for milestone in content_milestones: milestones_helpers.add_user_milestone(user, milestone) def handle_grade_event(block, event_type, event): # pylint: disable=unused-argument """ Manages the workflow for recording and updating of student module grade state """ user_id = event.get('user_id', user.id) grade = event.get('value') max_grade = event.get('max_value') set_score( user_id, descriptor.location, grade, max_grade, ) # Bin score into range and increment stats score_bucket = get_score_bucket(grade, max_grade) tags = [ u"org:{}".format(course_id.org), u"course:{}".format(course_id), u"score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) # Cycle through the milestone fulfillment scenarios to see if any are now applicable # thanks to the updated grading information that was just submitted _fulfill_content_milestones( user, course_id, descriptor.location, ) # Send a signal out to any listeners who are waiting for score change # events. SCORE_CHANGED.send(sender=None, points_possible=event['max_value'], points_earned=event['value'], user_id=user_id, course_id=unicode(course_id), usage_id=unicode(descriptor.location)) def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade' and not is_masquerading_as_specific_student( user, course_id): handle_grade_event(block, event_type, event) else: track_function(event_type, event) def rebind_noauth_module_to_user(module, real_user): """ A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser. Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler. Arguments: module (any xblock type): the module to rebind real_user (django.contrib.auth.models.User): the user to bind to Returns: nothing (but the side effect is that module is re-bound to real_user) """ if user.is_authenticated(): err_msg = ( "rebind_noauth_module_to_user can only be called from a module bound to " "an anonymous user") log.error(err_msg) raise LmsModuleRenderError(err_msg) field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents( course_id, real_user, module.descriptor, asides=XBlockAsidesConfig.possible_asides(), ) student_data_real_user = KvsFieldData( DjangoKeyValueStore(field_data_cache_real_user)) (inner_system, inner_student_data) = get_module_system_for_user( user=real_user, student_data= student_data_real_user, # These have implicit user bindings, rest of args considered not to descriptor=module.descriptor, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course) module.descriptor.bind_for_student( inner_system, real_user.id, [ partial(OverrideFieldData.wrap, real_user, course), partial(LmsFieldData, student_data=inner_student_data), ], ) module.descriptor.scope_ids = ( module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access ) module.scope_ids = module.descriptor.scope_ids # this is needed b/c NamedTuples are immutable # now bind the module to the new ModuleSystem instance and vice-versa module.runtime = inner_system inner_system.xmodule_instance = module # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] if is_masquerading_as_specific_student(user, course_id): block_wrappers.append(filter_displayed_blocks) if settings.FEATURES.get("LICENSING", False): block_wrappers.append(wrap_with_license) # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append( partial( wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes( usage_id.to_deprecated_string()), request_token=request_token, )) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append( partial(replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append( partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id.to_deprecated_string(), 'module_id': '' }), )) if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'): if is_masquerading_as_specific_student(user, course_id): # When masquerading as a specific student, we want to show the debug button # unconditionally to enable resetting the state of the student we are masquerading as. # We already know the user has staff access when masquerading is active. staff_access = True # To figure out whether the user has instructor access, we temporarily remove the # masquerade_settings from the real_user. With the masquerading settings in place, # the result would always be "False". masquerade_settings = user.real_user.masquerade_settings del user.real_user.masquerade_settings instructor_access = has_access(user.real_user, 'instructor', descriptor, course_id) user.real_user.masquerade_settings = masquerade_settings else: staff_access = has_access(user, 'staff', descriptor, course_id) instructor_access = has_access(user, 'instructor', descriptor, course_id) if staff_access: block_wrappers.append( partial(add_staff_markup, user, instructor_access, disable_staff_debug_info)) # These modules store data using the anonymous_student_id as a key. # To prevent loss of data, we will continue to provide old modules with # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance( descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) module_class = getattr(descriptor, 'module_class', None) is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, None) field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access user_is_staff = has_access(user, u'staff', descriptor.location, course_id) system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_key=course_id), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={ 'course_id': course_id.to_deprecated_string(), 'module_id': '' })), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), get_python_lib_zip=( lambda: get_python_lib_zip(contentstore, course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ 'i18n': ModuleI18nService(), 'fs': FSService(), 'field-data': field_data, 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), "reverification": ReverificationService() }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, request_token=request_token, ) # pass position specified in URL to module through ModuleSystem if position is not None: try: position = int(position) except (ValueError, TypeError): log.exception('Non-integer %r passed as position.', position) position = None system.set('position', position) if settings.FEATURES.get( 'ENABLE_PSYCHOMETRICS') and user.is_authenticated(): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location)) system.set(u'user_is_staff', user_is_staff) system.set(u'user_is_admin', has_access(user, u'staff', 'global')) system.set(u'user_is_beta_tester', CourseBetaTesterRole(course_id).has_user(user)) system.set(u'days_early_for_beta', getattr(descriptor, 'days_early_for_beta')) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, u'staff', descriptor.location, course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor return system, field_data
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, model_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type) def xblock_model_data(descriptor): return DbModel( LmsKeyValueStore(descriptor._model_data, model_data_cache), descriptor.module_class, user.id, LmsUsage(descriptor.location, descriptor.location)) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return usage = LmsUsage(descriptor.location, descriptor.location) # Construct the key for the module key = KeyValueStore.Key(scope=Scope.user_state, student_id=user.id, block_scope_id=usage.id, field_name='grade') student_module = model_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), ), node_path=settings.NODE_PATH, xblock_model_data=xblock_model_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception( "Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display is True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), course_namespace=module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) # this will rewrite intra-courseware links # that use the shorthand /jump_to_id/<id>. This is very helpful # for studio authored courses (compared to the /course/... format) since it is # is durable with respect to moves and the author doesn't need to # know the hierarchy # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work module.get_html = replace_jump_to_id_urls( module.get_html, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) # force the module to save after rendering module.get_html = save_module(module.get_html, module) return module
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument,redefined-outer-name """ Returns true if the specified user has staff access. """ return bool(user and has_access(user, 'staff', course, course.id))
def can_access_all_blocks(requesting_user, course_key): """ Returns whether the requesting_user can access all the blocks in the course. """ return has_access(requesting_user, CourseStaffRole.ROLE, course_key)
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Verify that the student has access to this thread if belongs to a discussion module if discussion_id not in utils.get_discussion_categories_ids( course, request.user): raise Http404 # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit")) except cc.utils.CommentClientRequestError as e: if e.status_code == 404: raise Http404 raise # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in threads ] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': _attr_safe_json(user_info), 'can_create_comment': _attr_safe_json( has_permission(request.user, "create_comment", course.id)), 'can_create_subcomment': _attr_safe_json( has_permission(request.user, "create_sub_comment", course.id)), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'annotated_content_info': _attr_safe_json(annotated_content_info), 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string( ), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': _attr_safe_json(threads), 'roles': _attr_safe_json(utils.get_role_ids(course_key)), 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course)), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': _attr_safe_json(course_settings) } return render_to_response('discussion/index.html', context)
def can_access_others_blocks(requesting_user, course_key): """ Returns whether the requesting_user can access the blocks for other users in the given course. """ return has_access(requesting_user, CourseStaffRole.ROLE, course_key)
def should_remove(self, user): """ Test to see if this result should be removed due to access restriction """ return not has_access(user, "load", self.get_item( self.get_usage_key()), self.get_course_key())
def is_enabled(cls, course, user=None): return user and user.is_authenticated and \ bool(CourseEnrollment.is_enrolled(user, course.id) or has_access(user, 'staff', course, course.id))
def test__user_passed_as_none(self): """Ensure has_access handles a user being passed as null""" access.has_access(None, 'staff', 'global', None)
def __iter__(self): def parent_or_requested_block_type(usage_key): """ Returns whether the usage_key's block_type is one of self.block_types or a parent type. """ return (usage_key.block_type in self.block_types or usage_key.block_type in BLOCK_TYPES_WITH_CHILDREN) def create_module(descriptor): """ Factory method for creating and binding a module for the given descriptor. """ field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_id, self.request.user, descriptor, depth=0, ) course = get_course_by_id(self.course_id) return get_module_for_descriptor(self.request.user, self.request, descriptor, field_data_cache, self.course_id, course=course) with modulestore().bulk_operations(self.course_id): child_to_parent = {} stack = [self.start_block] while stack: curr_block = stack.pop() if curr_block.hide_from_toc: # For now, if the 'hide_from_toc' setting is set on the block, do not traverse down # the hierarchy. The reason being is that these blocks may not have human-readable names # to display on the mobile clients. # Eventually, we'll need to figure out how we want these blocks to be displayed on the # mobile clients. As they are still accessible in the browser, just not navigatable # from the table-of-contents. continue if curr_block.location.block_type in self.block_types: if not has_access(self.request.user, 'load', curr_block, course_key=self.course_id): continue summary_fn = self.block_types[curr_block.category] block_path = list( path(curr_block, child_to_parent, self.start_block)) unit_url, section_url = find_urls(self.course_id, curr_block, child_to_parent, self.request) yield { "path": block_path, "named_path": [b["name"] for b in block_path], "unit_url": unit_url, "section_url": section_url, "summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache) } if curr_block.has_children: children = get_dynamic_descriptor_children( curr_block, self.request.user.id, create_module, usage_key_filter=parent_or_requested_block_type) for block in reversed(children): stack.append(block) child_to_parent[block] = curr_block
def courses(request, show_hidden): """ Renders the courses list for the API. :param request: The django HttpRequest object. :param show_hidden: True or False, (controlled from the urls.py file) to show courses with upcoming enrollment date. :return: JsonResponse with a list of the courses. """ courses_list = branding.get_visible_courses() if not show_hidden: # Using `AnonymousUser()` to hide unpublished courses anonymous_user = AnonymousUser() # The logic bellow has been copied (with amendments) from `courseware.courses.get_courses`, # Just in case something changes with edX releases. permission_name = settings.COURSE_CATALOG_VISIBILITY_PERMISSION courses_list = [ c for c in courses_list if has_access(anonymous_user, permission_name, c) ] courses_list = sort_by_announcement(courses_list) courses_list = edraak_courses_logic(courses_list) courses_json_list = [] prefix = get_absolute_url_prefix(request) for course_overview in courses_list: try: course = get_course(course_overview.id) video_tag = get_course_about_section(request, course, "video") youtube_id = video_tag[video_tag.find("embed") + 6:video_tag.find("?")] courses_json_list.append({ "id": unicode(course.id), "number": course.display_number_with_default, "name": course.display_name_with_default_escaped, "organization": course.display_org_with_default, "description": get_course_about_section(request, course, "short_description").strip(), "startDate": course.start, "endDate": course.end, "enrollmentStartDate": course.enrollment_start, "enrollmentEndDate": course.enrollment_end, "overview": get_course_about_section(request, course, "overview").strip(), "aboutPage": prefix + reverse('about_course', args=[unicode(course.id)]), "image": prefix + course_image_url(course), "state": _get_course_status(course), "youtube_id": youtube_id, "effort": get_course_about_section(request, course, "effort").strip(), }) except ValueError: log.error(u"Course with id '{0}' not found".format( course_overview.id)) return JsonResponse(courses_json_list)
def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = CourseKey.from_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'audit': # If the learner has arrived at this screen via the traditional enrollment workflow, # then they should already be enrolled in an audit mode for the course, assuming one has # been configured. However, alternative enrollment workflows have been introduced into the # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) # If the course has started redirect to course home instead if course.has_started(): return redirect( reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, mode=requested_mode) # If the course has started redirect to course home instead if course.has_started(): return redirect( reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}))
def single_thread(request, course_id, discussion_id, thread_id): nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, course_id, 'load_forum') cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_id, thread, request.user, user_info=user_info) context = {'thread': thread.to_dict(), 'course_id': course_id} content = utils.safe_content(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: with newrelic.agent.FunctionTrace(nr_transaction, "get_discussion_category_map"): category_map = utils.get_discussion_category_map(course) threads, query_params = get_threads(request, course_id) threads.append(thread.to_dict()) course = get_course_with_access(request.user, course_id, 'load_forum') with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course) for thread in threads: if thread.get('group_id') and not thread.get('group_name'): thread['group_name'] = get_cohort_by_id( course_id, thread.get('group_id')).name #patch for backward compatibility with comments service if not "pinned" in thread: thread["pinned"] = False threads = [utils.safe_content(thread) for thread in threads] #recent_active_threads = cc.search_recent_active_threads( # course_id, # recursive=False, # query_params={'follower_id': request.user.id}, #) #trending_tags = cc.search_trending_tags( # course_id, #) with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_id, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): cohorts = get_course_cohorts(course_id) cohorted_commentables = get_cohorted_commentables(course_id) user_cohort = get_cohort_id(request.user, course_id) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': saxutils.escape(json.dumps(user_info), escapedict), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), 'course': course, #'recent_active_threads': recent_active_threads, #'trending_tags': trending_tags, 'course_id': course.id, # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': saxutils.escape(json.dumps(threads), escapedict), 'category_map': category_map, 'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict), 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_id), 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id), 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'), 'cohorts': cohorts, 'user_cohort': get_cohort_id(request.user, course_id), 'cohorted_commentables': cohorted_commentables } return render_to_response('discussion/single_thread.html', context)
def index(request, course_id): # Palette color_not = '#CCCCCC' color_fail = '#e41a1c' color_ok = '#F2F20D' color_prof = '#4daf4a' problem_activity='#377eb8' video_activity='#ff7f00' video_repetition='#fdbf6f' course_activity='#984ea3' graded_time='#88419d' ungraded_time='#8c6bb1' chapter_time='#8c96c6' play_event='#1b9e77' pause_event='#d95f02' seek_from_event='#7570b3' seek_to_event='#e7298a' change_speed_event='#66a61e' morning_time='#C9C96C' afternoon_time ='#7F7160' night_time ='#50587C' # Request data course_key = get_course_key(course_id) course = get_course_module(course_key) #course2= get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) #user = request.user #Codigo Jose A. Gascon staff_access = has_access(request.user, 'staff', course).has_access#Codigo Jose A. Gascon instructor_access = has_access(request.user, 'instructor', course).has_access#Codigo Jose A. Gascon #Codigo Jose A. Gascon masq, user = setup_masquerade(request, course_key,staff_access, reset_masquerade_data=True) # allow staff to toggle masquerade on info page user = request.user studio_url = get_studio_url(course, 'course_info') #reverifications = fetch_reverify_banner_info(request, course_key) #course = get_course_with_access(request.user, action='load', course_key=course_key, depth=None, check_if_enrolled=False) #user = User.objects.get(request.user.email) # Proficiency and pass limit pass_limit = get_course_grade_cutoff(course) proficiency_limit = (1 - pass_limit) / 2 + pass_limit usernames_in = [] for student in CourseEnrollment.objects.users_enrolled_in(course_key):#Codigo Jose A. Gascon, se cambia la forma de llamar al metode users_enrolled_in usernames_in.append(student.username.encode('utf-8')) # Data for visualization in JSON user_for_charts = '#average' if (staff_access or instructor_access) else user kwargs = { 'qualifiers': {'category': 'video', }, } # This returns video descriptors in the order they appear on the course video_descriptors = videos_problems_in(course)[0] #WARNINIG #video_durations = get_info_videos_descriptors(video_descriptors)[2] #video_names, video_module_keys, video_durations = get_info_videos_descriptors(video_descriptors) # NO SE USAN LAS OTRAS VARIABLES video_names, video_module_keys, video_durations =get_DB_infovideos(course_key) video_names_sorted = video_names video_ids_sort = video_names_sorted #course_name = get_course_by_id(course_key, depth=None) names_students=[] only_students = [] students_names = get_course_students(course_key) print students_names for student in students_names: staff_access_user = has_access(student, 'staff', course).has_access instructor_access_user = has_access(student, 'instructor', course).has_access if not (staff_access_user or instructor_access_user): names_students.append(student.username.encode('utf-8')) only_students.append(student) video_ids_str = [] course_video_names = [] problem_ids_str=[] for descriptor in video_descriptors: video_ids_str.append((course_key.make_usage_key('video', descriptor.location.name))._to_string()) course_video_names.append(unicodedata.normalize('NFKD', descriptor.display_name_with_default).encode('ASCII', 'ignore')) if len(video_descriptors) > 0: first_video_id = course_key.make_usage_key('video', video_descriptors[0].location.name) # Video progress visualization. Video percentage seen total and non-overlapped. video_names, avg_video_time, video_percentages = get_video_time_watched(user_for_charts, course_key) if avg_video_time != []: all_video_time_percent = map(truediv, avg_video_time, video_durations) all_video_time_percent = [int(round(x*100,0)) for x in all_video_time_percent] else: all_video_time_percent = avg_video_time column_headers = ['Video', 'Different video time', 'Total video time'] # Codigo Javier Orcoyen video_prog_json = ready_for_arraytodatatable(column_headers, video_names, video_percentages, all_video_time_percent) video_names, all_video_time = get_module_consumption(user_for_charts, course_key, 'video') # Time spent on every video resource column_headers = ['Video', 'Time watched'] video_distrib_json = ready_for_arraytodatatable(column_headers, video_names, all_video_time) # Video events dispersion within video length scatter_array = get_video_events_info(user_for_charts, first_video_id) # Repetitions per video intervals user_for_vid_intervals = '#class_total_times' if user_for_charts == '#average' else user_for_charts video_intervals_array = get_user_video_intervals(user_for_vid_intervals, first_video_id) # Case no videos in course else: video_names = None video_prog_json = json.dumps(None) video_distrib_json = json.dumps(None) scatter_array = json.dumps(None) video_intervals_array = json.dumps(None) # Time spent on every problem resource # Codigo Javier Orcoyen problem_names, time_x_problem = get_module_consumption(user_for_charts, course_key, 'problem') column_headers = ['Problem', 'Time on problem'] problem_distrib_json = ready_for_arraytodatatable(column_headers, problem_names, time_x_problem) print 'USER' print user problems_in = videos_problems_in(course)[1] problem_names_sorted = [x.display_name_with_default.encode('utf-8') for x in problems_in] orden=[] orden.append(i for i, x in enumerate(problem_names_sorted)) problem_ids_str=list(problem_names_sorted) for n in range(len(problem_names_sorted)): problem_names_sorted[n]= unicodedata.normalize('NFKD', unicode(problem_names_sorted[n],'utf-8')).encode('ASCII', 'ignore') problem_ids_str[n]= unicodedata.normalize('NFKD', unicode(problem_ids_str[n],'utf-8')).encode('ASCII', 'ignore') user_val=1 # Daily time spent on video and/or problem resources video_days, video_daily_time = get_daily_consumption(user_for_charts, course_key, 'video') problem_days, problem_daily_time = get_daily_consumption(user_for_charts, course_key, 'problem') vid_and_prob_daily_time = join_video_problem_time(video_days, video_daily_time, problem_days, problem_daily_time) #Analytics visualizations if staff_access or instructor_access: # Instructor access std_sort = get_DB_sort_course_homework(course_key) # Chapter time cs, st = get_DB_course_spent_time(course_key, student_id=ALL_STUDENTS) students_spent_time = chapter_time_to_js(cs, st) students_grades = get_DB_student_grades(course_key, student_id=ALL_STUDENTS) cs, sa = course_accesses = get_DB_course_section_accesses(course_key, student_id=ALL_STUDENTS) students_course_accesses = course_accesses_to_js(cs, sa) #students_prob_vid_progress = get_DB_course_video_problem_progress(course_key, student_id=ALL_STUDENTS)# C. J. A. Gascon ERROR students_time_schedule = get_DB_time_schedule(course_key, student_id=ALL_STUDENTS) else: # Sort homework # Chapter time std_sort = None cs, st = get_DB_course_spent_time(course_key, user.id) students_spent_time = chapter_time_to_js(cs, st) students_grades = get_DB_student_grades(course_key, user.id) cs, sa = course_accesses = get_DB_course_section_accesses(course_key, user.id) students_course_accesses = course_accesses_to_js(cs, sa) students_time_schedule = get_DB_time_schedule(course_key, user.id) #students_prob_vid_progress = get_DB_course_video_problem_progress(course_key, user.id) #C. J. A. Gascon ERROR context = {'course': course, 'request': request, 'user': user, 'user_id': user.id, 'staff_access': staff_access, 'instructor_access': instructor_access, 'masquerade': masq, 'studio_url': studio_url, #'reverifications': reverifications, 'course_id': course_id, 'students': students_to_js(only_students), 'visualizations_id': VISUALIZATIONS_ID, 'std_grades_dump': dumps(students_grades), 'sort_std_dump': dumps(std_sort), 'time_dump': dumps(students_spent_time), 'accesses_dump': dumps(students_course_accesses), 'std_time_schedule_dumb': dumps(students_time_schedule), #'vid_prob_prog_dump': dumps(students_prob_vid_progress), #C. J. A. Gascon ERROR 'pass_limit': pass_limit, 'prof_limit': proficiency_limit, 'usernames_in' : usernames_in, 'video_names' : course_video_names, 'video_ids' : video_ids_str, 'video_prog_json' : video_prog_json, 'video_distrib_json' : video_distrib_json, 'problem_distrib_json' : problem_distrib_json, 'video_intervals_array' : video_intervals_array, 'vid_and_prob_daily_time' : vid_and_prob_daily_time, 'scatter_array' : scatter_array, 'problem_names' : problem_names, 'problem_ids' : problem_ids_str, 'color_not' : color_not, 'color_ok' : color_ok, 'color_prof' : color_prof, 'color_fail' : color_fail, 'problem_activity' : problem_activity, 'video_activity' : video_activity, 'course_activity' : course_activity, 'video_repetition' : video_repetition, 'graded_time' : graded_time, 'ungraded_time' : ungraded_time, 'chapter_time' : chapter_time, 'user_for_charts' : user_for_charts, 'video_ids_sort' : video_ids_sort, 'video_names_sorted' : video_names_sorted, 'problem_names_sorted' : problem_names_sorted, 'play_event' : play_event, 'pause_event' : pause_event, 'seek_from_event' : seek_from_event, 'seek_to_event' : seek_to_event, 'change_speed_event' : change_speed_event, 'morning_time' : morning_time, 'afternoon_time' : afternoon_time, 'night_time' : night_time, 'names_students' : names_students, 'user_val' : user_val,} return render_to_response('learning_analytics/learning_analytics.html', context)
def render_to_fragment(self, request, course_id=None, **kwargs): """ Renders the course's home page as a fragment. """ course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) # Render the course dates as a fragment dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id, **kwargs) # Render the full content to enrolled users, as well as to course and global staff. # Unenrolled users who are not course or global staff are given only a subset. enrollment = CourseEnrollment.get_enrollment(request.user, course_key) user_access = { 'is_anonymous': request.user.is_anonymous(), 'is_enrolled': enrollment is not None, 'is_staff': has_access(request.user, 'staff', course_key), } if user_access['is_enrolled'] or user_access['is_staff']: outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs) if LATEST_UPDATE_FLAG.is_enabled(course_key): update_message_fragment = LatestUpdateFragmentView().render_to_fragment( request, course_id=course_id, **kwargs ) else: update_message_fragment = WelcomeMessageFragmentView().render_to_fragment( request, course_id=course_id, **kwargs ) course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs) has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id) else: # Redirect the user to the dashboard if they are not enrolled and # this is a course that does not support direct enrollment. if not can_self_enroll_in_course(course_key): raise CourseAccessRedirect(reverse('dashboard')) # Set all the fragments outline_fragment = None update_message_fragment = None course_sock_fragment = None has_visited_course = None resume_course_url = None # Get the handouts handouts_html = self._get_course_handouts(request, course) # Get the course tools enabled for this user and course course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key) # Grab the course home messages fragment to render any relevant django messages course_home_message_fragment = CourseHomeMessageFragmentView().render_to_fragment( request, course_id=course_id, user_access=user_access, **kwargs ) # Get info for upgrade messaging upgrade_price = None upgrade_url = None # TODO Add switch to control deployment if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled(course_key) and enrollment and enrollment.upgrade_deadline: verified_mode = enrollment.verified_mode if verified_mode: upgrade_price = verified_mode.min_price ecommerce_service = EcommerceService() if ecommerce_service.is_enabled(request.user): upgrade_url = ecommerce_service.get_checkout_page_url(verified_mode.sku) else: upgrade_url = reverse('verify_student_upgrade_and_verify', args=(course_key,)) # Render the course home fragment context = { 'request': request, 'csrf': csrf(request)['csrf_token'], 'course': course, 'course_key': course_key, 'outline_fragment': outline_fragment, 'handouts_html': handouts_html, 'course_home_message_fragment': course_home_message_fragment, 'has_visited_course': has_visited_course, 'resume_course_url': resume_course_url, 'course_tools': course_tools, 'dates_fragment': dates_fragment, 'update_message_fragment': update_message_fragment, 'course_sock_fragment': course_sock_fragment, 'disable_courseware_js': True, 'uses_pattern_library': True, 'upgrade_price': upgrade_price, 'upgrade_url': upgrade_url, } html = render_to_string('course_experience/course-home-fragment.html', context) return Fragment(html)
def student_dashboard(request): """ Provides the LMS dashboard view TODO: This is lms specific and does not belong in common code. Arguments: request: The request object. Returns: The dashboard response. """ user = request.user if not UserProfile.objects.filter(user=user).exists(): return redirect(reverse('account_settings')) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) enable_verified_certificates = configuration_helpers.get_value( 'ENABLE_VERIFIED_CERTIFICATES', settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES')) display_course_modes_on_dashboard = configuration_helpers.get_value( 'DISPLAY_COURSE_MODES_ON_DASHBOARD', settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True)) activation_email_support_link = configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK hide_dashboard_courses_until_activated = configuration_helpers.get_value( 'HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', settings.FEATURES.get('HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', False)) empty_dashboard_message = configuration_helpers.get_value( 'EMPTY_DASHBOARD_MESSAGE', None) # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site( ) course_enrollments = list( get_course_enrollments(user, site_org_whitelist, site_org_blacklist)) # Get the entitlements for the user and a mapping to all available sessions for that entitlement # If an entitlement has no available sessions, pass through a mock course overview object (course_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions ) = get_filtered_course_entitlements(user, site_org_whitelist, site_org_blacklist) # Record how many courses there are so that we can get a better # understanding of usage patterns on prod. monitoring_utils.accumulate('num_courses', len(course_enrollments)) # Sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) # Retrieve the course modes for each course enrolled_course_ids = [ enrollment.course_id for enrollment in course_enrollments ] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses( enrolled_course_ids) course_modes_by_course = { course_id: {mode.slug: mode for mode in modes} for course_id, modes in iteritems(unexpired_course_modes) } # Check to see if the student has recently enrolled in a course. # If so, display a notification message confirming the enrollment. enrollment_message = _create_recent_enrollment_message( course_enrollments, course_modes_by_course) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) # Display activation message activate_account_message = '' if not user.is_active: activate_account_message = Text( _("Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " "If you need help, contact {link_start}{platform_name} Support{link_end}." ) ).format( platform_name=platform_name, email_start=HTML("<strong>"), email_end=HTML("</strong>"), email=user.email, link_start=HTML( "<a target='_blank' href='{activation_email_support_link}'>"). format( activation_email_support_link=activation_email_support_link, ), link_end=HTML("</a>"), ) enterprise_message = get_dashboard_consent_notification( request, user, course_enrollments) # Disable lookup of Enterprise consent_required_course due to ENT-727 # Will re-enable after fixing WL-1315 consent_required_courses = set() enterprise_customer_name = None # Account activation message account_activation_messages = [ message for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Global staff can see what courses encountered an error on their dashboard staff_access = False errored_courses = {} if has_access(user, 'staff', 'global'): # Show any courses that encountered an error on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = { enrollment.course_id: has_access(request.user, 'load', enrollment.course_overview) for enrollment in course_enrollments } # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) ecommerce_service = EcommerceService() inverted_programs = meter.invert_programs() urls, programs_data = {}, {} bundles_on_dashboard_flag = WaffleFlag( WaffleFlagNamespace(name=u'student.experiments'), u'bundles_on_dashboard') # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete if bundles_on_dashboard_flag.is_enabled( ) and inverted_programs and inverted_programs.items(): if len(course_enrollments) < 4: for program in inverted_programs.values(): try: program_uuid = program[0]['uuid'] program_data = get_programs(request.site, uuid=program_uuid) program_data = ProgramDataExtender(program_data, request.user).extend() skus = program_data.get('skus') checkout_page_url = ecommerce_service.get_checkout_page_url( *skus) program_data[ 'completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get( 'uuid') programs_data[program_uuid] = program_data except: # pylint: disable=bare-except pass # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict # we loaded earlier to avoid hitting the database. course_mode_info = { enrollment.course_id: complete_course_mode_info( enrollment.course_id, enrollment, modes=course_modes_by_course[enrollment.course_id]) for enrollment in course_enrollments } # Determine the per-course verification status # This is a dictionary in which the keys are course locators # and the values are one of: # # VERIFY_STATUS_NEED_TO_VERIFY # VERIFY_STATUS_SUBMITTED # VERIFY_STATUS_APPROVED # VERIFY_STATUS_MISSED_DEADLINE # # Each of which correspond to a particular message to display # next to the course on the dashboard. # # If a course is not included in this dictionary, # there is no verification messaging to display. verify_status_by_course = check_verify_status_by_course( user, course_enrollments) cert_statuses = { enrollment.course_id: cert_info(request.user, enrollment.course_overview) for enrollment in course_enrollments } # only show email settings for Mongo course and when bulk email is turned on show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if (BulkEmailFlag.feature_enabled(enrollment.course_id))) # Verification Attempts # Used to generate the "you must reverify for course x" banner verification_status = IDVerificationService.user_status(user) verification_errors = get_verification_error_reasons_for_display( verification_status['error']) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( request, CourseRegistrationCode.objects.filter( course_id=enrollment.course_id, registrationcoderedemption__redeemed_by=request.user), enrollment.course_id)) enrolled_courses_either_paid = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.is_paid_course()) # If there are *any* denied reverifications that have not been toggled off, # we'll display the banner denied_banner = any(item.display for item in reverifications["denied"]) # Populate the Order History for the side-bar. order_history_list = order_history(user, course_org_filter=site_org_whitelist, org_filter_out_set=site_org_blacklist) # get list of courses having pre-requisites yet to be completed courses_having_prerequisites = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.course_overview.pre_requisite_courses) courses_requirements_not_met = get_pre_requisite_courses_not_completed( user, courses_having_prerequisites) if 'notlive' in request.GET: redirect_message = _( "The course you are looking for does not start until {date}." ).format(date=request.GET['notlive']) elif 'course_closed' in request.GET: redirect_message = _( "The course you are looking for is closed for enrollment as of {date}." ).format(date=request.GET['course_closed']) elif 'access_response_error' in request.GET: # This can be populated in a generalized way with fields from access response errors redirect_message = request.GET['access_response_error'] else: redirect_message = '' valid_verification_statuses = [ 'approved', 'must_reverify', 'pending', 'expired' ] display_sidebar_on_dashboard = ( len(order_history_list) or (verification_status['status'] in valid_verification_statuses and verification_status['should_display'])) # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [ e for e in course_entitlements if e.enrollment_course_run is not None ]: course_enrollments = [ enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] context = { 'urls': urls, 'programs_data': programs_data, 'enterprise_message': enterprise_message, 'consent_required_courses': consent_required_courses, 'enterprise_customer_name': enterprise_customer_name, 'enrollment_message': enrollment_message, 'redirect_message': redirect_message, 'account_activation_messages': account_activation_messages, 'activate_account_message': activate_account_message, 'course_enrollments': course_enrollments, 'course_entitlements': course_entitlements, 'course_entitlement_available_sessions': course_entitlement_available_sessions, 'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions, 'course_optouts': course_optouts, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'all_course_modes': course_mode_info, 'cert_statuses': cert_statuses, 'credit_statuses': _credit_statuses(user, course_enrollments), 'show_email_settings_for': show_email_settings_for, 'reverifications': reverifications, 'verification_display': verification_status['should_display'], 'verification_status': verification_status['status'], 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, 'inverted_programs': inverted_programs, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_journal_listing': journals_enabled(), # TODO: Dashboard Plugin required 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_sidebar_on_dashboard': display_sidebar_on_dashboard, 'display_sidebar_account_activation_message': not (user.is_active or hide_dashboard_courses_until_activated), 'display_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated), 'empty_dashboard_message': empty_dashboard_message, } if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, 'ecommerce_payment_page': ecommerce_service.payment_page_url(), }) # Gather urls for course card resume buttons. resume_button_urls = ['' for entitlement in course_entitlements] for url in _get_urls_for_resume_buttons(user, course_enrollments): resume_button_urls.append(url) # There must be enough urls for dashboard.html. Template creates course # cards for "enrollments + entitlements". context.update({'resume_button_urls': resume_button_urls}) response = render_to_response('dashboard.html', context) set_logged_in_cookies(request, response, user) return response
def get_module_system_for_user(user, student_data, # TODO # pylint: disable=too-many-statements # Arguments preceding this comment have user binding, those following don't descriptor, course_id, track_function, xqueue_callback_url_prefix, request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path='', user_location=None, disable_staff_debug_info=False, course=None): """ Helper function that returns a module system and student_data bound to a user and a descriptor. The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module, to allow an existing module to be re-bound to a user. Most of the user bindings happen when creating the closures that feed the instantiation of ModuleSystem. The arguments fall into two categories: those that have explicit or implicit user binding, which are user and student_data, and those don't and are just present so that ModuleSystem can be instantiated, which are all the other arguments. Ultimately, this isn't too different than how get_module_for_descriptor_internal was before refactoring. Arguments: see arguments for get_module() request_token (str): A token unique to the request use by xblock initialization Returns: (LmsModuleSystem, KvsFieldData): (module system, student_data) bound to, primarily, the user and descriptor """ def make_xqueue_callback(dispatch='score_update'): """ Returns fully qualified callback URL for external queueing system """ relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict( course_id=course_id.to_deprecated_string(), userid=str(user.id), mod_id=descriptor.location.to_deprecated_string(), dispatch=dispatch ), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': XQUEUE_INTERFACE, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user=user, descriptor=descriptor, student_data=student_data, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course ) def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id): SCORE_PUBLISHED.send( sender=None, block=block, user=user, raw_earned=event['value'], raw_possible=event['max_value'], only_if_higher=event.get('only_if_higher'), score_deleted=event.get('score_deleted'), ) else: context = contexts.course_context_from_course_id(course_id) if block.runtime.user_id: context['user_id'] = block.runtime.user_id context['asides'] = {} for aside in block.runtime.get_asides(block): if hasattr(aside, 'get_event_context'): aside_event_info = aside.get_event_context(event_type, event) if aside_event_info is not None: context['asides'][aside.scope_ids.block_type] = aside_event_info with tracker.get_tracker().context(event_type, context): track_function(event_type, event) def rebind_noauth_module_to_user(module, real_user): """ A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser. Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler. Arguments: module (any xblock type): the module to rebind real_user (django.contrib.auth.models.User): the user to bind to Returns: nothing (but the side effect is that module is re-bound to real_user) """ if user.is_authenticated(): err_msg = ("rebind_noauth_module_to_user can only be called from a module bound to " "an anonymous user") log.error(err_msg) raise LmsModuleRenderError(err_msg) field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents( course_id, real_user, module.descriptor, asides=XBlockAsidesConfig.possible_asides(), ) student_data_real_user = KvsFieldData(DjangoKeyValueStore(field_data_cache_real_user)) (inner_system, inner_student_data) = get_module_system_for_user( user=real_user, student_data=student_data_real_user, # These have implicit user bindings, rest of args considered not to descriptor=module.descriptor, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course ) module.descriptor.bind_for_student( inner_system, real_user.id, [ partial(OverrideFieldData.wrap, real_user, course), partial(LmsFieldData, student_data=inner_student_data), ], ) module.descriptor.scope_ids = ( module.descriptor.scope_ids._replace(user_id=real_user.id) ) module.scope_ids = module.descriptor.scope_ids # this is needed b/c NamedTuples are immutable # now bind the module to the new ModuleSystem instance and vice-versa module.runtime = inner_system inner_system.xmodule_instance = module # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] if is_masquerading_as_specific_student(user, course_id): block_wrappers.append(filter_displayed_blocks) if settings.FEATURES.get("LICENSING", False): block_wrappers.append(wrap_with_license) # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append(partial( wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()), request_token=request_token, )) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append(partial( replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path )) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append(partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}), )) if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'): if is_masquerading_as_specific_student(user, course_id): # When masquerading as a specific student, we want to show the debug button # unconditionally to enable resetting the state of the student we are masquerading as. # We already know the user has staff access when masquerading is active. staff_access = True # To figure out whether the user has instructor access, we temporarily remove the # masquerade_settings from the real_user. With the masquerading settings in place, # the result would always be "False". masquerade_settings = user.real_user.masquerade_settings del user.real_user.masquerade_settings instructor_access = bool(has_access(user.real_user, 'instructor', descriptor, course_id)) user.real_user.masquerade_settings = masquerade_settings else: staff_access = has_access(user, 'staff', descriptor, course_id) instructor_access = bool(has_access(user, 'instructor', descriptor, course_id)) if staff_access: block_wrappers.append(partial(add_staff_markup, user, instructor_access, disable_staff_debug_info)) # These modules store data using the anonymous_student_id as a key. # To prevent loss of data, we will continue to provide old modules with # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) module_class = getattr(descriptor, 'module_class', None) is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, None) field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access user_is_staff = bool(has_access(user, u'staff', descriptor.location, course_id)) system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, course_key=course_id ), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}) ), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ 'fs': FSService(), 'field-data': field_data, 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), 'verification': VerificationService(), 'proctoring': ProctoringService(), 'milestones': milestones_helpers.get_service(), 'credit': CreditService(), 'bookmarks': BookmarksService(user=user), }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, request_token=request_token, ) # pass position specified in URL to module through ModuleSystem if position is not None: try: position = int(position) except (ValueError, TypeError): log.exception('Non-integer %r passed as position.', position) position = None system.set('position', position) system.set(u'user_is_staff', user_is_staff) system.set(u'user_is_admin', bool(has_access(user, u'staff', 'global'))) system.set(u'user_is_beta_tester', CourseBetaTesterRole(course_id).has_user(user)) system.set(u'days_early_for_beta', descriptor.days_early_for_beta) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, u'staff', descriptor.location, course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor return system, field_data
def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_info=False, course=None): """ Gets a module instance based on its `usage_id` in a course, for a given request/user Returns (instance, tracking_context) """ user = request.user try: course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) usage_key = course_id.make_usage_key_from_deprecated_string( unquote_slashes(usage_id)) except InvalidKeyError: raise Http404("Invalid location") try: descriptor = modulestore().get_item(usage_key) descriptor_orig_usage_key, descriptor_orig_version = modulestore( ).get_block_original_usage(usage_key) except ItemNotFoundError: log.warn("Invalid location for course id %s: %s", usage_key.course_key, usage_key) raise Http404 tracking_context = { 'module': { 'display_name': descriptor.display_name_with_default, 'usage_key': unicode(descriptor.location), } } # For blocks that are inherited from a content library, we add some additional metadata: if descriptor_orig_usage_key is not None: tracking_context['module']['original_usage_key'] = unicode( descriptor_orig_usage_key) tracking_context['module']['original_usage_version'] = unicode( descriptor_orig_version) unused_masquerade, user = setup_masquerade( request, course_id, has_access(user, 'staff', descriptor, course_id)) field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, user, descriptor) instance = get_module_for_descriptor( user, request, descriptor, field_data_cache, usage_key.course_key, disable_staff_debug_info=disable_staff_debug_info, course=course) if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. log.debug("No module %s for user %s -- access denied?", usage_key, user) raise Http404 return (instance, tracking_context)