def _setstaff_login(self): """Makes the test user staff and logs them in""" GlobalStaff().add_users(self.user) self.client.login(username=self.user.username, password='******')
def test_request_global_staff_courses_using_scope(self): GlobalStaff().add_users(self.user) self._assert_role_using_scope('course_staff', 'staff_courses', assert_one_course=False)
def test_request_global_staff_courses_with_claims(self): GlobalStaff().add_users(self.user) self._assert_role_using_claim('course_staff', 'staff_courses')
def certificates_list_handler(request, course_key_string): """ A RESTful handler for Course Certificates GET html: return Certificates list page (Backbone application) POST json: create new Certificate """ course_key = CourseKey.from_string(course_key_string) store = modulestore() with store.bulk_operations(course_key): try: course = _get_course_and_check_access(course_key, request.user) except PermissionDenied: msg = _('PermissionDenied: Failed in authenticating {user}').format(user=request.user) return JsonResponse({"error": msg}, status=403) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): certificate_url = reverse_course_url('certificates.certificates_list_handler', course_key) course_outline_url = reverse_course_url('course_handler', course_key) upload_asset_url = reverse_course_url('assets_handler', course_key) activation_handler_url = reverse_course_url( handler_name='certificates.certificate_activation_handler', course_key=course_key ) course_modes = [mode.slug for mode in CourseMode.modes_for_course(course.id)] certificate_web_view_url = get_lms_link_for_certificate_web_view( user_id=request.user.id, course_key=course_key, mode=course_modes[0] # CourseMode.modes_for_course returns default mode 'honor' if doesn't find anyone. ) certificates = None is_active = False if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): certificates = CertificateManager.get_certificates(course) # we are assuming only one certificate in certificates collection. for certificate in certificates: is_active = certificate.get('is_active', False) break return render_to_response('certificates.html', { 'context_course': course, 'certificate_url': certificate_url, 'course_outline_url': course_outline_url, 'upload_asset_url': upload_asset_url, 'certificates': json.dumps(certificates), 'course_modes': course_modes, 'certificate_web_view_url': certificate_web_view_url, 'is_active': is_active, 'is_global_staff': GlobalStaff().has_user(request.user), 'certificate_activation_handler_url': activation_handler_url }) elif "application/json" in request.META.get('HTTP_ACCEPT'): # Retrieve the list of certificates for the specified course if request.method == 'GET': certificates = CertificateManager.get_certificates(course) return JsonResponse(certificates, encoder=EdxJSONEncoder) elif request.method == 'POST': # Add a new certificate to the specified course try: new_certificate = CertificateManager.deserialize_certificate(course, request.body) except CertificateValidationError as err: return JsonResponse({"error": err.message}, status=400) if course.certificates.get('certificates') is None: course.certificates['certificates'] = [] course.certificates['certificates'].append(new_certificate.certificate_data) response = JsonResponse(CertificateManager.serialize_certificate(new_certificate), status=201) response["Location"] = reverse_course_url( 'certificates.certificates_detail_handler', course.id, kwargs={'certificate_id': new_certificate.id} ) store.update_item(course, request.user.id) CertificateManager.track_event('created', { 'course_id': unicode(course.id), 'configuration_id': new_certificate.id }) course = _get_course_and_check_access(course_key, request.user) return response else: return HttpResponse(status=406)
def certificates_detail_handler(request, course_key_string, certificate_id): """ JSON API endpoint for manipulating a course certificate via its internal identifier. Utilized by the Backbone.js 'certificates' application model POST or PUT json: update the specified certificate based on provided information DELETE json: remove the specified certificate from the course """ course_key = CourseKey.from_string(course_key_string) course = _get_course_and_check_access(course_key, request.user) certificates_list = course.certificates.get('certificates', []) match_index = None match_cert = None for index, cert in enumerate(certificates_list): if certificate_id is not None: if int(cert['id']) == int(certificate_id): match_index = index match_cert = cert store = modulestore() if request.method in ('POST', 'PUT'): if certificate_id: active_certificates = CertificateManager.get_certificates(course, only_active=True) if int(certificate_id) in [int(certificate["id"]) for certificate in active_certificates]: # Only global staff (PMs) are able to edit active certificate configuration if not GlobalStaff().has_user(request.user): raise PermissionDenied() try: new_certificate = CertificateManager.deserialize_certificate(course, request.body) except CertificateValidationError as err: return JsonResponse({"error": err.message}, status=400) serialized_certificate = CertificateManager.serialize_certificate(new_certificate) cert_event_type = 'created' if match_cert: cert_event_type = 'modified' certificates_list[match_index] = serialized_certificate else: certificates_list.append(serialized_certificate) store.update_item(course, request.user.id) CertificateManager.track_event(cert_event_type, { 'course_id': unicode(course.id), 'configuration_id': serialized_certificate["id"] }) return JsonResponse(serialized_certificate, status=201) elif request.method == "DELETE": if not match_cert: return JsonResponse(status=404) active_certificates = CertificateManager.get_certificates(course, only_active=True) if int(certificate_id) in [int(certificate["id"]) for certificate in active_certificates]: # Only global staff (PMs) are able to delete active certificate configuration if not GlobalStaff().has_user(request.user): raise PermissionDenied() CertificateManager.remove_certificate( request=request, store=store, course=course, certificate_id=certificate_id ) CertificateManager.track_event('deleted', { 'course_id': unicode(course.id), 'configuration_id': certificate_id }) return JsonResponse(status=204)
def check_support(): """Check that the user has access to the support UI. """ if perm != 'global': return ACCESS_DENIED return (ACCESS_GRANTED if GlobalStaff().has_user(user) or SupportStaffRole().has_user(user) else ACCESS_DENIED)
def set_staff(self, create, extracted, **kwargs): GlobalStaff().add_users(self)
def check_staff(): if perm != 'global': debug("Deny: invalid permission '%s'", perm) return False return GlobalStaff().has_user(user)
def get_blacklist_of_fields(cls, course_key): """ Returns a list of fields to not include in Studio Advanced settings based on a feature flag (i.e. enabled or disabled). """ # Copy the filtered list to avoid permanently changing the class attribute. black_list = list(cls.FIELDS_BLACK_LIST) # Do not show giturl if feature is not enabled. if not settings.FEATURES.get('ENABLE_EXPORT_GIT'): black_list.append('giturl') # Do not show edxnotes if the feature is disabled. if not settings.FEATURES.get('ENABLE_EDXNOTES'): black_list.append('edxnotes') # Do not show video auto advance if the feature is disabled if not settings.FEATURES.get('ENABLE_OTHER_COURSE_SETTINGS'): black_list.append('other_course_settings') # Do not show video_upload_pipeline if the feature is disabled. if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'): black_list.append('video_upload_pipeline') # Do not show video auto advance if the feature is disabled if not settings.FEATURES.get('ENABLE_AUTOADVANCE_VIDEOS'): black_list.append('video_auto_advance') # Do not show social sharing url field if the feature is disabled. if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")): black_list.append('social_sharing_url') # Do not show teams configuration if feature is disabled. if not settings.FEATURES.get('ENABLE_TEAMS'): black_list.append('teams_configuration') if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): black_list.append('video_bumper') # Do not show enable_ccx if feature is not enabled. if not settings.FEATURES.get('CUSTOM_COURSES_EDX'): black_list.append('enable_ccx') black_list.append('ccx_connector') # Do not show "Issue Open Badges" in Studio Advanced Settings # if the feature is disabled. if not settings.FEATURES.get('ENABLE_OPENBADGES'): black_list.append('issue_badges') # If the XBlockStudioConfiguration table is not being used, there is no need to # display the "Allow Unsupported XBlocks" setting. if not XBlockStudioConfigurationFlag.is_enabled(): black_list.append('allow_unsupported_xblocks') # If the ENABLE_PROCTORING_PROVIDER_OVERRIDES waffle flag is not enabled, # do not show "Proctoring Configuration" in Studio Advanced Settings. if not ENABLE_PROCTORING_PROVIDER_OVERRIDES.is_enabled(course_key): black_list.append('proctoring_provider') # Do not show "Course Visibility For Unenrolled Learners" in Studio Advanced Settings # if the enable_anonymous_access flag is not enabled if not COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key=course_key): black_list.append('course_visibility') # Do not show "Create Zendesk Tickets For Suspicious Proctored Exam Attempts" in # Studio Advanced Settings if the user is not edX staff. if not GlobalStaff().has_user(get_current_user()): black_list.append('create_zendesk_tickets') return black_list
def post(self, request): # pylint: disable=too-many-statements """Enrolls the currently logged-in user in a course. Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. """ # Get the User, Course ID, and Mode from the request. username = request.data.get('user', request.user.username) course_id = request.data.get('course_details', {}).get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"Course ID must be specified to create a new enrollment."} ) try: course_id = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) mode = request.data.get('mode') has_api_key_permissions = self.has_api_key_permissions(request) # Check that the user specified is either the same user, or this is a server-to-server request. if not username: username = request.user.username if username != request.user.username and not has_api_key_permissions \ and not GlobalStaff().has_user(request.user): # Return a 404 instead of a 403 (Unauthorized). If one user is looking up # other users, do not let them deduce the existence of an enrollment. return Response(status=status.HTTP_404_NOT_FOUND) if mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) and not has_api_key_permissions \ and not GlobalStaff().has_user(request.user): return Response( status=status.HTTP_403_FORBIDDEN, data={ "message": u"User does not have permission to create enrollment with mode [{mode}].".format( mode=mode ) } ) try: # Lookup the user, instead of using request.user, since request.user may not match the username POSTed. user = User.objects.get(username=username) except ObjectDoesNotExist: return Response( status=status.HTTP_406_NOT_ACCEPTABLE, data={ 'message': u'The user {} does not exist.'.format(username) } ) embargo_response = embargo_api.get_embargo_response(request, course_id, user) if embargo_response: return embargo_response try: is_active = request.data.get('is_active') # Check if the requested activation status is None or a Boolean if is_active is not None and not isinstance(is_active, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enrollment activation status.").format(value=is_active) } ) explicit_linked_enterprise = request.data.get('linked_enterprise_customer') if explicit_linked_enterprise and has_api_key_permissions and enterprise_enabled(): enterprise_api_client = EnterpriseApiServiceClient() consent_client = ConsentApiServiceClient() try: enterprise_api_client.post_enterprise_course_enrollment(username, text_type(course_id), None) except EnterpriseApiException as error: log.exception(u"An unexpected error occurred while creating the new EnterpriseCourseEnrollment " u"for user [%s] in course run [%s]", username, course_id) raise CourseEnrollmentError(text_type(error)) kwargs = { 'username': username, 'course_id': text_type(course_id), 'enterprise_customer_uuid': explicit_linked_enterprise, } consent_client.provide_consent(**kwargs) enrollment_attributes = request.data.get('enrollment_attributes') enrollment = api.get_enrollment(username, text_type(course_id)) mode_changed = enrollment and mode is not None and enrollment['mode'] != mode active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active missing_attrs = [] if enrollment_attributes: actual_attrs = [ u"{namespace}:{name}".format(**attr) for attr in enrollment_attributes ] missing_attrs = set(REQUIRED_ATTRIBUTES.get(mode, [])) - set(actual_attrs) if (GlobalStaff().has_user(request.user) or has_api_key_permissions) and (mode_changed or active_changed): if mode_changed and active_changed and not is_active: # if the requester wanted to deactivate but specified the wrong mode, fail # the request (on the assumption that the requester had outdated information # about the currently active enrollment). msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format( enrollment["mode"], mode ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) if missing_attrs: msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format( mode, REQUIRED_ATTRIBUTES.get(mode) ) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) response = api.update_enrollment( username, text_type(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes, # If we are updating enrollment by authorized api caller, we should allow expired modes include_expired=has_api_key_permissions ) else: # Will reactivate inactive enrollments. response = api.add_enrollment( username, text_type(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes ) cohort_name = request.data.get('cohort') if cohort_name is not None: cohort = get_cohort_by_name(course_id, cohort_name) try: add_user_to_cohort(cohort, user) except ValueError: # user already in cohort, probably because they were un-enrolled and re-enrolled log.exception('Cohort re-addition') email_opt_in = request.data.get('email_opt_in', None) if email_opt_in is not None: org = course_id.org update_email_opt_in(request.user, org, email_opt_in) log.info(u'The user [%s] has already been enrolled in course run [%s].', username, course_id) return Response(response) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]." ).format(mode=mode, course_id=course_id), "course_details": error.data }) except CourseNotFoundError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) except CourseEnrollmentExistsError as error: log.warning(u'An enrollment already exists for user [%s] in course run [%s].', username, course_id) return Response(data=error.enrollment) except CourseEnrollmentError: log.exception(u"An error occurred while creating the new course enrollment for user " u"[%s] in course run [%s]", username, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": ( u"An error occurred while creating the new course enrollment for user " u"'{username}' in course '{course_id}'" ).format(username=username, course_id=course_id) } ) except CourseUserGroup.DoesNotExist: log.exception(u'Missing cohort [%s] in course run [%s]', cohort_name, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"An error occured while adding to cohort [%s]" % cohort_name }) finally: # Assumes that the ecommerce service uses an API key to authenticate. if has_api_key_permissions: current_enrollment = api.get_enrollment(username, text_type(course_id)) audit_log( 'enrollment_change_requested', course_id=text_type(course_id), requested_mode=mode, actual_mode=current_enrollment['mode'] if current_enrollment else None, requested_activation=is_active, actual_activation=current_enrollment['is_active'] if current_enrollment else None, user_id=user.id )
def test_global_staff(self): self.assertFalse(GlobalStaff().has_user(self.student)) self.assertFalse(GlobalStaff().has_user(self.course_staff)) self.assertFalse(GlobalStaff().has_user(self.course_instructor)) self.assertTrue(GlobalStaff().has_user(self.global_staff))
def certificates_list_handler(request, course_key_string): """ A RESTful handler for Course Certificates GET html: return Certificates list page (Backbone application) POST json: create new Certificate """ course_key = CourseKey.from_string(course_key_string) store = modulestore() with store.bulk_operations(course_key): try: course = _get_course_and_check_access(course_key, request.user) except PermissionDenied: msg = _(u'PermissionDenied: Failed in authenticating {user}').format(user=request.user) return JsonResponse({"error": msg}, status=403) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): certificate_url = reverse_course_url('certificates_list_handler', course_key) course_outline_url = reverse_course_url('course_handler', course_key) upload_asset_url = reverse_course_url('assets_handler', course_key) activation_handler_url = reverse_course_url( handler_name='certificate_activation_handler', course_key=course_key ) course_modes = [ mode.slug for mode in CourseMode.modes_for_course( course_id=course.id, include_expired=True ) if mode.slug != 'audit' ] has_certificate_modes = len(course_modes) > 0 if has_certificate_modes: certificate_web_view_url = get_lms_link_for_certificate_web_view( course_key=course_key, mode=course_modes[0] # CourseMode.modes_for_course returns default mode if doesn't find anyone. ) else: certificate_web_view_url = None is_active, certificates = CertificateManager.is_activated(course) course_authoring_microfrontend_url = get_proctored_exam_settings_url(course) return render_to_response('certificates.html', { 'context_course': course, 'certificate_url': certificate_url, 'course_outline_url': course_outline_url, 'upload_asset_url': upload_asset_url, 'certificates': certificates, 'has_certificate_modes': has_certificate_modes, 'course_modes': course_modes, 'certificate_web_view_url': certificate_web_view_url, 'is_active': is_active, 'is_global_staff': GlobalStaff().has_user(request.user), 'certificate_activation_handler_url': activation_handler_url, 'course_authoring_microfrontend_url': course_authoring_microfrontend_url, }) elif "application/json" in request.META.get('HTTP_ACCEPT'): # Retrieve the list of certificates for the specified course if request.method == 'GET': certificates = CertificateManager.get_certificates(course) return JsonResponse(certificates, encoder=EdxJSONEncoder) elif request.method == 'POST': # Add a new certificate to the specified course try: new_certificate = CertificateManager.deserialize_certificate(course, request.body) except CertificateValidationError as err: return JsonResponse({"error": text_type(err)}, status=400) if course.certificates.get('certificates') is None: course.certificates['certificates'] = [] course.certificates['certificates'].append(new_certificate.certificate_data) response = JsonResponse(CertificateManager.serialize_certificate(new_certificate), status=201) response["Location"] = reverse_course_url( 'certificates_detail_handler', course.id, kwargs={'certificate_id': new_certificate.id} ) store.update_item(course, request.user.id) CertificateManager.track_event('created', { 'course_id': six.text_type(course.id), 'configuration_id': new_certificate.id }) course = _get_course_and_check_access(course_key, request.user) return response else: return HttpResponse(status=406)