def make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super().make_ccx(max_students_allowed=max_students_allowed) ccx.structure_json = json.dumps(self.master_course_chapters) ccx.save() override_field_for_ccx(ccx, self.course, 'start', now()) override_field_for_ccx(ccx, self.course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in self.course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) # enroll the coach in the CCX ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) email_params = get_email_params( self.course, auto_enroll=True, course_key=ccx_course_key, display_name=ccx.display_name ) enroll_email( course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, email_students=False, email_params=email_params, ) return ccx
def auto_enroll_email(course_id, email, send_email=True): """ Auto-enroll email in course. Based on lms.djangoapps.instructor.views.api.students_update_enrollment() """ # Raises ValidationError if invalid validate_email(email) locator = CourseLocator.from_string(course_id) course = get_course_by_id(locator) # If we want to notify the newly enrolled student by email, fetch # the required parameters email_params = None language = None if send_email: email_params = get_email_params(course, True, secure=True) # Try to find out what language to send the email in. user = None try: user = User.objects.get(email=email) except User.DoesNotExist: pass else: language = get_user_email_language(user) # Enroll the email enroll_email(locator, email, auto_enroll=True, email_students=send_email, email_params=email_params, language=language)
def _change_access(course, user, level, action, send_email=True): """ Change access of user. `level` is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] NOTE: will create a group if it does not yet exist. """ try: role = ROLES[level](course.id) except KeyError: raise ValueError(u"unrecognized level '{}'".format(level)) if action == 'allow': if level == 'ccx_coach': email_params = get_email_params(course, True) enroll_email( course_id=course.id, student_email=user.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) role.add_users(user) elif action == 'revoke': role.remove_users(user) else: raise ValueError(u"unrecognized action '{}'".format(action))
def ccx_student_management(request, course, ccx=None): """ Manage the enrollment of individual students in a CCX """ if not ccx: raise Http404 action = request.POST.get('student-action', None) student_id = request.POST.get('student-id', '') email_students = 'email-students' in request.POST identifiers = [student_id] course_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) email_params = get_email_params( course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach) for error_message in errors: messages.error(request, error_message) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key}) return redirect(url)
def ccx_student_management(request, course, ccx=None): """ Manage the enrollment of individual students in a CCX """ if not ccx: raise Http404 action = request.POST.get('student-action', None) student_id = request.POST.get('student-id', '') email_students = 'email-students' in request.POST identifiers = [student_id] course_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach) for error_message in errors: messages.error(request, error_message) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key}) return redirect(url)
def _change_access(course, user, level, action, send_email=True): """ Change access of user. `level` is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] NOTE: will create a group if it does not yet exist. """ try: role = ROLES[level](course.id) except KeyError: raise ValueError("unrecognized level '{}'".format(level)) if action == 'allow': if level == 'ccx_coach': email_params = get_email_params(course, True) enroll_email( course_id=course.id, student_email=user.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) role.add_users(user) elif action == 'revoke': role.remove_users(user) else: raise ValueError("unrecognized action '{}'".format(action))
def put(self, request, course_id): """ Enroll a user in a course; requires staff access **Example Request** PUT /api/enrollment/v1/roster/course-v1:foo+bar+foobar { 'email': '*****@*****.**', 'email_students': false, 'auto_enroll': true } """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid or missing course_id', }, ) if not user_has_role(request.user, CourseStaffRole(course_key)): return Response( status=status.HTTP_403_FORBIDDEN, data={ 'message': u'User does not have permission to update enrollment for [{course_id}].' .format(course_id=course_id, ), }, ) email = request.data.get('email') try: validate_email(email) except ValidationError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid email address', }, ) email_students = request.data.get('email_students', False) in ['true', 'True', True] auto_enroll = request.data.get('auto_enroll', False) in ['true', 'True', True] email_params = {} language = None if email_students: course = get_course_by_id(course_key) email_params = get_email_params(course, auto_enroll) if User.objects.filter(email=email).exists(): user = User.objects.get(email=email) language = get_user_email_language(user) enroll_email(course_key, email, auto_enroll, email_students, email_params, language=language) return Response(status=status.HTTP_204_NO_CONTENT)
def get_email_params(self): """ Returns a dictionary of parameters used to render an email. """ email_params = get_email_params(self.course, True) email_params["email_address"] = "*****@*****.**" email_params["full_name"] = "Jean Reno" return email_params
def test_normal_params(self): # For a normal site, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, False) self.assertEqual(result['auto_enroll'], False) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
def test_marketing_params(self): # For a site with a marketing front end, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): result = get_email_params(self.course, True) self.assertEqual(result['auto_enroll'], True) # We should *not* get a course about url (LMS doesn't know what the marketing site URLs are) self.assertEqual(result['course_about_url'], None) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
def get_email_params_ccx(self): """ Returns a dictionary of parameters used to render an email for CCX. """ email_params = get_email_params(self.course, True, course_key=self.course_key, display_name=self.ccx.display_name) email_params["email_address"] = "*****@*****.**" email_params["full_name"] = "Jean Reno" return email_params
def test_ccx_enrollment_email_params(self): # For a CCX, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, True, course_key=self.course_key, display_name=self.ccx.display_name) self.assertEqual(result['display_name'], self.ccx.display_name) self.assertEqual(result['auto_enroll'], True) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
def get_email_params_ccx(self): """ Returns a dictionary of parameters used to render an email for CCX. """ email_params = get_email_params( self.course, True, course_key=self.course_key, display_name=self.ccx.display_name ) email_params["email_address"] = "*****@*****.**" email_params["full_name"] = "Jean Reno" return email_params
def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, send_email=True): """ Remove staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseBlockWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on revoke access. """ list_staff = list_with_level(master_course.id, 'staff') list_instructor = list_with_level(master_course.id, 'instructor') with ccx_course(ccx_key) as course_ccx: list_staff_ccx = list_with_level(course_ccx.id, 'staff') list_instructor_ccx = list_with_level(course_ccx.id, 'instructor') email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) for staff in list_staff: if staff in list_staff_ccx: # revoke 'staff' access on ccx. revoke_access(course_ccx, staff, 'staff') # Unenroll the staff on ccx. unenroll_email( course_id=ccx_key, student_email=staff.email, email_students=send_email, email_params=email_params, ) for instructor in list_instructor: if instructor in list_instructor_ccx: # revoke 'instructor' access on ccx. revoke_access(course_ccx, instructor, 'instructor') # Unenroll the instructor on ccx. unenroll_email( course_id=ccx_key, student_email=instructor.email, email_students=send_email, email_params=email_params, )
def test_ccx_enrollment_email_params(self): # For a CCX, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params( self.course, True, course_key=self.course_key, display_name=self.ccx.display_name ) self.assertEqual(result['display_name'], self.ccx.display_name) self.assertEqual(result['auto_enroll'], True) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
def api_params_helper(self, request, course_key, email): """ Helper method to get params for enroll/unenroll apis. """ email_students = request.data.get('email_students', False) in ['true', 'True', True] auto_enroll = request.data.get('auto_enroll', True) in ['true', 'True', True] email_params = {} language = None if email_students: course = get_course_by_id(course_key) email_params = get_email_params(course, auto_enroll) try: user = User.objects.get(email=email) except User.DoesNotExist: pass else: language = get_user_email_language(user) return email_students, auto_enroll, email_params, language
def ccx_invite(request, course, ccx=None): """ Invite users to new ccx """ if not ccx: raise Http404 action = request.POST.get('enrollment-button') identifiers_raw = request.POST.get('student-ids') identifiers = _split_input_list(identifiers_raw) email_students = 'email-students' in request.POST course_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key}) return redirect(url)
def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, send_email=True): """ Remove staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on revoke access. """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: list_staff_ccx = list_with_level(course_ccx, 'staff') list_instructor_ccx = list_with_level(course_ccx, 'instructor') email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) for staff in list_staff: if staff in list_staff_ccx: # revoke 'staff' access on ccx. revoke_access(course_ccx, staff, 'staff') # Unenroll the staff on ccx. unenroll_email( course_id=ccx_key, student_email=staff.email, email_students=send_email, email_params=email_params, ) for instructor in list_instructor: if instructor in list_instructor_ccx: # revoke 'instructor' access on ccx. revoke_access(course_ccx, instructor, 'instructor') # Unenroll the instructor on ccx. unenroll_email( course_id=ccx_key, student_email=instructor.email, email_students=send_email, email_params=email_params, )
def ccx_students_management(request, course, ccx=None): """ Manage the enrollment of the students in a CCX """ if not ccx: raise Http404 action, identifiers = get_enrollment_action_and_identifiers(request) email_students = 'email-students' in request.POST course_key = CCXLocator.from_course_locator(course.id, six.text_type(ccx.id)) email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach) for error_message in errors: messages.error(request, error_message) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key}) return redirect(url)
def delete_redemption_entry(request, code_redemption, course_key): """ delete the redemption entry from the table and unenroll the user who used the registration code for the enrollment and send him/her the unenrollment email. """ user = code_redemption.redeemed_by email_address = code_redemption.redeemed_by.email full_name = code_redemption.redeemed_by.profile.name CourseEnrollment.unenroll(user, course_key, skip_refund=True) course = get_course_by_id(course_key, depth=0) email_params = get_email_params(course, True, secure=request.is_secure()) email_params['message_type'] = 'enrolled_unenroll' email_params['email_address'] = email_address email_params['full_name'] = full_name send_mail_to_student(email_address, email_params) # remove the redemption entry from the database. log.info('deleting redemption entry (%s) from the database.', code_redemption.id) code_redemption.delete()
def delete_redemption_entry(request, code_redemption, course_key): """ delete the redemption entry from the table and unenroll the user who used the registration code for the enrollment and send him/her the unenrollment email. """ user = code_redemption.redeemed_by email_address = code_redemption.redeemed_by.email full_name = code_redemption.redeemed_by.profile.name CourseEnrollment.unenroll(user, course_key, skip_refund=True) course = get_course_by_id(course_key, depth=0) email_params = get_email_params(course, True, secure=request.is_secure()) email_params['message_type'] = 'enrolled_unenroll' email_params['email_address'] = email_address email_params['full_name'] = full_name send_mail_to_student(email_address, email_params) # remove the redemption entry from the database. log.info(u'deleting redemption entry (%s) from the database.', code_redemption.id) code_redemption.delete()
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error( request, _("You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX(course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Save display name explicitly override_field_for_ccx(ccx, course, 'display_name', name) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, str(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) assign_staff_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, str(ccx.id))) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return redirect(url)
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error(request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX( course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) assign_staff_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, unicode(ccx.id)) ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return redirect(url)
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object( ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response(status=http_status, data={'error_code': error_code}) master_course_id = request.data.get('master_course_id') if master_course_id is not None and str( ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={'error_code': 'master_course_id_change_not_allowed'}) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course( str(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps( valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx(ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response(status=status.HTTP_204_NO_CONTENT, )
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True) if master_course_object is None: return Response(status=http_status, data={'error_code': error_code}) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) try: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator( master_course_object.id, str(ccx_course_object.id)) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign staff role for the coach to the newly created ccx assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx(master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False) serializer = self.get_serializer(ccx_course_object) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response(status=status.HTTP_201_CREATED, data=serializer.data)
def enroll_user_to_course(request_info, course_id, username_or_email): """ Look up the given user, and if successful, enroll them to the specified course. Arguments: request_info (dict): Dict containing task request information course_id (str): The ID string of the course username_or_email: user's username or email string Returns: User object (or None if user in not registered, and whether the user is already enrolled or not """ # First try to get a user object from the identifier (email) user = None user_already_enrolled = False language = None email_students = True auto_enroll = True thread_site = Site.objects.get(domain=request_info['host']) thread_author = User.objects.get(username=request_info['username']) try: user = get_student_from_identifier(username_or_email) except User.DoesNotExist: email = username_or_email else: email = user.email language = get_user_email_language(user) if user: course_enrollment = CourseEnrollment.get_enrollment( user=user, course_key=course_id) if course_enrollment: user_already_enrolled = True # Set the enrollment to active if its not already active if not course_enrollment.is_active: course_enrollment.update_enrollment(is_active=True) if not user or not user_already_enrolled: course = get_course_by_id(course_id, depth=0) try: with emulate_http_request(site=thread_site, user=thread_author): email_params = get_email_params(course, auto_enroll) __ = enroll_email(course_id, email, auto_enroll, email_students, email_params, language=language) if user: TASK_LOG.info( u'User %s enrolled successfully in course %s via CSV bulk enrollment', username_or_email, course_id) except: TASK_LOG.exception( u'There was an error enrolling user %s in course %s via CSV bulk enrollment', username_or_email, course_id) return None, None return user, user_already_enrolled
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) master_course_id = request.data.get('master_course_id') if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={ 'error_code': 'master_course_id_change_not_allowed' } ) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps(valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx( ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response( status=status.HTTP_204_NO_CONTENT, )
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True ) if master_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) try: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json ) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx( ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, unicode(ccx_course_object.id)) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign staff role for the coach to the newly created ccx assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx( master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False ) serializer = self.get_serializer(ccx_course_object) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response( status=status.HTTP_201_CREATED, data=serializer.data )
def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_email=True): """ Add staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on access grant. """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) list_staff_ccx = list_with_level(course_ccx, 'staff') list_instructor_ccx = list_with_level(course_ccx, 'instructor') for staff in list_staff: # this call should be idempotent if staff not in list_staff_ccx: try: # Enroll the staff in the ccx enroll_email( course_id=ccx_key, student_email=staff.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'staff' access on ccx to staff of master course allow_access(course_ccx, staff, 'staff') except CourseEnrollmentException: log.warning( "Unable to enroll staff %s to course with id %s", staff.email, ccx_key ) continue except SMTPException: continue for instructor in list_instructor: # this call should be idempotent if instructor not in list_instructor_ccx: try: # Enroll the instructor in the ccx enroll_email( course_id=ccx_key, student_email=instructor.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'instructor' access on ccx to instructor of master course allow_access(course_ccx, instructor, 'instructor') except CourseEnrollmentException: log.warning( "Unable to enroll instructor %s to course with id %s", instructor.email, ccx_key ) continue except SMTPException: continue
def task_generate_user(self): task_input = self.request valid_rows = task_input.get("valid_rows") microsite = task_input.get("microsite") requester_id = task_input.get("requester_id") _requester_user = User.objects.get(pk=requester_id) self.site_name = task_input.get('site_name') + ' ' log.warning( u'tma_dashboard.task_generate_user inscription users pour le microsite : ' + microsite) log.warning( u'tma_dashboard.task_generate_user inscription users par le username ' + _requester_user.username + ' email : ' + _requester_user.email) generated_passwords = [] _generates = [] _failed = [] warnings = [] #Get all keys from register form register_keys = [] register_form = task_input.get("register_form") for _key in register_form: register_keys.append(_key.get('name')) # for white labels we use 'shopping cart' which uses CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG as # course mode for creating course enrollments. if CourseMode.is_white_label(self.course_key): course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG else: course_mode = None #TREATING EACH USER for _user in valid_rows: #get current users values try: email = _user.get('email') username = email.split('@')[0].replace('-', '').replace( '.', '').replace('_', '')[0:10] + '_' + random_string(5) first_name = str(_user.get("first_name")) last_name = str(_user.get("last_name")) complete_name = first_name + ' ' + last_name #check valid email email_params = get_email_params(self.course, True, secure=True) new_course_url = 'https://' + self.site_name.replace( ' ', '') + '/dashboard/' + str(self.course.id) email_params.update({ 'site_name': self.site_name, 'course_url': new_course_url, }) except: _failed.append({ 'email': email, 'response': _('Invalid info {email_address}.').format( email_address=email) }) try: validate_email(email) except ValidationError: _failed.append({ 'email': email, 'response': _('Invalid email {email_address}.').format( email_address=email) }) if User.objects.filter(email=email).exists(): # ENROLL EXISTING USER TO COURSE user = User.objects.get(email=email) # see if it is an exact match with email and username if it's not an exact match then just display a warning message, but continue onwards if not User.objects.filter(email=email, username=username).exists(): warning_message = _( 'An account with email {email} exists but the provided username {username} ' 'is different. Enrolling anyway with {email}.').format( email=email, username=username) warnings.append({ 'username': username, 'email': email, 'response': warning_message }) log.warning(u'email %s already exist', email) else: log.info( u"user already exists with username '%s' and email '%s'", username, email) # enroll a user if it is not already enrolled. if not CourseEnrollment.is_enrolled(user, self.course_key): create_manual_course_enrollment( user=user, course_id=self.course_key, mode=course_mode, enrolled_by=_requester_user, reason='Enrolling via csv upload', state_transition=UNENROLLED_TO_ENROLLED, ) enroll_email(course_id=self.course_key, student_email=email, auto_enroll=True, email_students=True, email_params=email_params) else: # CREATE NEW ACCOUNT password = self.generate_unique_password(generated_passwords) #generate custom_field custom_field = {} for key, value in _user.items(): #assurer que la key est presente dans la liste des key et non presente dans les custom_fields actuels if (key in register_keys) and (not key in custom_field.keys()): custom_field[key] = value created_user = self.create_and_enroll_user( email, username, custom_field, password, complete_name, self.course_id, course_mode, _requester_user, email_params, first_name, last_name) #maj de l'info if created_user != '': _generates.append({ "id": created_user.id, "email": created_user.email }) else: _failed.append({ "email": email, "reponse": "creation failed" }) log.warning( u'tma_dashboard.task_generate_user fin inscription users pour le microsite : ' + microsite) log.warning( u'tma_dashboard.task_generate_user fin inscription users par le username ' + _requester_user.username + ' email : ' + _requester_user.email) #Send an email to requester with potential failures status_text = '' if not _failed: status_text = 'Tous les utilisateurs ont bien été créés et/ou inscrits au cours.' else: status_text = "Une erreur s'est produite lors de l'inscription des utilisateurs suivants :<ul>" for user in _failed: status_text += "<li>" + user['email'] + "</li>" status_text += "</ul><p>Merci de remonter le problème au service IT pour identifier l'erreur sur ces profils. Les autres profils utilisateur ont été correctement créés et/ou inscrits au cours.</p>" course = get_course_by_id(self.course_key) html = "<html><head></head><body><p>Bonjour,<br><br> L'inscription par CSV de vos utilisateurs au cours " + course.display_name_with_default + " sur le microsite " + microsite + " est maintenant terminée.<br> " + status_text + "<br><br>The MOOC Agency<br></p></body></html>" part2 = MIMEText(html.encode('utf-8'), 'html', 'utf-8') fromaddr = "*****@*****.**" toaddr = _requester_user.email msg = MIMEMultipart() msg['From'] = fromaddr msg['To'] = toaddr msg['Subject'] = "Import utilisateurs csv" part = MIMEBase('application', 'octet-stream') server = smtplib.SMTP('mail3.themoocagency.com', 25) server.starttls() server.login('contact', 'waSwv6Eqer89') msg.attach(part2) text = msg.as_string() server.sendmail(fromaddr, toaddr, text) server.quit() retour = { "requester": _requester_user.email, "_generates": _generates, "_failed": _failed, "warning": warnings } return retour
def post(self, request, *args, **kwargs): course_id = request.POST.get('course_id', False) try: course_id = SlashSeparatedCourseKey.from_deprecated_string( course_id) except Exception: course_id = None if not course_id: self.msg = u"课程ID错误" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) elif not request.POST.get('identifiers'): self.msg = u"邮箱用户名错误" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) action = request.POST.get('action') identifiers_raw = request.POST.get('identifiers') identifiers = _split_input_list(identifiers_raw) auto_enroll = _get_boolean_param(request, 'auto_enroll') email_students = _get_boolean_param(request, 'email_students') is_white_label = CourseMode.is_white_label(course_id) reason = request.POST.get('reason') if is_white_label: if not reason: self.msg = "400" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) enrollment_obj = None state_transition = DEFAULT_TRANSITION_STATE email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll, secure=request.is_secure()) results = [] for identifier in identifiers: # First try to get a user object from the identifer user = None email = None language = None try: user = get_student_from_identifier(identifier) except User.DoesNotExist: email = identifier else: email = user.email language = get_user_email_language(user) try: # Use django.core.validators.validate_email to check email address # validity (obviously, cannot check if email actually /exists/, # simply that it is plausibly valid) validate_email(email) # Raises ValidationError if invalid if action == 'enroll': before, after, enrollment_obj = enroll_email( course_id, email, auto_enroll, email_students, email_params, language=language) before_enrollment = before.to_dict()['enrollment'] before_user_registered = before.to_dict()['user'] before_allowed = before.to_dict()['allowed'] after_enrollment = after.to_dict()['enrollment'] after_allowed = after.to_dict()['allowed'] if before_user_registered: if after_enrollment: if before_enrollment: state_transition = ENROLLED_TO_ENROLLED else: if before_allowed: state_transition = ALLOWEDTOENROLL_TO_ENROLLED else: state_transition = UNENROLLED_TO_ENROLLED else: if after_allowed: state_transition = UNENROLLED_TO_ALLOWEDTOENROLL elif action == 'unenroll': before, after = unenroll_email(course_id, email, email_students, email_params, language=language) before_enrollment = before.to_dict()['enrollment'] before_allowed = before.to_dict()['allowed'] enrollment_obj = CourseEnrollment.get_enrollment( user, course_id) if before_enrollment: state_transition = ENROLLED_TO_UNENROLLED else: if before_allowed: state_transition = ALLOWEDTOENROLL_TO_UNENROLLED else: state_transition = UNENROLLED_TO_UNENROLLED else: return HttpResponseBadRequest( strip_tags("Unrecognized action '{}'".format(action))) except ValidationError: # Flag this email as an error if invalid, but continue checking # the remaining in the list results.append({ 'identifier': identifier, 'invalidIdentifier': True, }) except Exception as exc: # pylint: disable=broad-except # catch and log any exceptions # so that one error doesn't cause a 500. log.exception(u"Error while #{}ing student") log.exception(exc) results.append({ 'identifier': identifier, 'error': True, }) else: ManualEnrollmentAudit.create_manual_enrollment_audit( request.user, email, state_transition, reason, enrollment_obj) results.append({ 'identifier': identifier, 'before': before.to_dict(), 'after': after.to_dict(), }) invalid_id = [] valid_id = [] for result in results: if ('error' in result) or ('invalidIdentifier' in result): invalid_id.append(result['identifier']) else: valid_id.append(result['identifier']) invalid_message = [ "{} 无效 <br>".format(i.encode('utf-8')) for i in invalid_id ] valid_message = [] action = "选课" if action == "enroll" else "弃选" for i in valid_id: if action == "弃选": valid_message.append("{0} {1} 成功 <br>".format(i, action)) continue if email_students: valid_message.append("{0} {1} 成功,并向他发送电子邮件 <br>".format( i, action)) else: valid_message.append("{0} {1} 成功<br>".format(i, action)) invalid_message.extend(valid_message) self.msg = "".join(invalid_message) context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context)
def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_email=True): """ Add staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseBlockWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on access grant. """ list_staff = list_with_level(master_course.id, 'staff') list_instructor = list_with_level(master_course.id, 'instructor') with ccx_course(ccx_key) as course_ccx: email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) list_staff_ccx = list_with_level(course_ccx.id, 'staff') list_instructor_ccx = list_with_level(course_ccx.id, 'instructor') for staff in list_staff: # this call should be idempotent if staff not in list_staff_ccx: try: # Enroll the staff in the ccx enroll_email( course_id=ccx_key, student_email=staff.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'staff' access on ccx to staff of master course allow_access(course_ccx, staff, 'staff') except CourseEnrollmentException: log.warning( "Unable to enroll staff %s to course with id %s", staff.email, ccx_key) continue except SMTPException: continue for instructor in list_instructor: # this call should be idempotent if instructor not in list_instructor_ccx: try: # Enroll the instructor in the ccx enroll_email( course_id=ccx_key, student_email=instructor.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'instructor' access on ccx to instructor of master course allow_access(course_ccx, instructor, 'instructor') except CourseEnrollmentException: log.warning( "Unable to enroll instructor %s to course with id %s", instructor.email, ccx_key) continue except SMTPException: continue