def create_canvas_course(sis_course_id, sis_user_id, bulk_job=None): """ This method creates a canvas course for the sis_course_id provided, initiated by the sis_user_id. The bulk_job_id would be passed in if it's invoked from a bulk feed process. """ # instantiate any variables required for method return or logger calls new_course = None section = None course_job_id = None # 1. Insert a CanvasCourseGenerationJob record on initiation with STATUS_SETUP status. This would help in # keeping track of the status of the various courses in the bulk job context as well as general reporting # if there's no bulk id, we need to create the CanvasCourseGenerationJob bulk_job_id = None template_id = None if bulk_job: bulk_job_id = bulk_job.id template_id = bulk_job.template_canvas_course_id try: course_generation_job = CanvasCourseGenerationJob.objects.filter( sis_course_id=sis_course_id, bulk_job_id=bulk_job_id).get() except Exception as e: ex = CourseGenerationJobNotFoundError(msg_details=(bulk_job_id, sis_course_id)) logger.exception(ex.display_text) raise ex else: try: logger.debug('Create content migration job tracking row...') course_generation_job = CanvasCourseGenerationJob.objects.create( sis_course_id=sis_course_id, created_by_user_id=sis_user_id, workflow_state=CanvasCourseGenerationJob.STATUS_SETUP, ) course_job_id = course_generation_job.pk logger.debug('Job row created: %s' % course_generation_job) except Exception as e: logger.exception('Error in inserting CanvasCourseGenerationJob record for ' 'with sis_course_id=%s: exception=%s' % (sis_course_id, e)) # send email in addition to showing error page to user ex = CourseGenerationJobCreationError(msg_details=sis_course_id) send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex try: # 2. fetch the course instance info course_data = get_course_data(sis_course_id) logger.info("\n obtained course info for ci=%s, acct_id=%s, course_name=%s, code=%s, term=%s, section_name=%s\n" % (course_data, course_data.sis_account_id, course_data.course_name, course_data.course_code, course_data.sis_term_id, course_data.primary_section_name())) except ObjectDoesNotExist as e: logger.error('ObjectDoesNotExist exception when fetching SIS data for course ' 'with sis_course_id=%s: exception=%s' % (sis_course_id, e)) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state( sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SISCourseDoesNotExistError(sis_course_id) # If the course is part of bulk job, do not send individual email. . if not bulk_job_id: msg = ex.display_text # TLT-393: send an email to support group, in addition to showing error page to user send_failure_msg_to_support(sis_course_id, sis_user_id, msg) raise ex # If the account ID begins with dept: then check if department already exists in Canvas if course_data.sis_account_id.startswith('dept:'): # TLT-3689 Check to see if the department exists in Canvas # If it does not, then create it try: get_single_account(request_ctx=SDK_CONTEXT, id='sis_account_id:%s' % course_data.sis_account_id) except CanvasAPIError: department_id = course_data.sis_account_id.replace('dept:', '') department = Department.objects.get(department_id=department_id) logger.info("Department does not exist for {}, creating one now".format(course_data.sis_account_id)) # It seems that using the sis_account_id:xxx in create_new_sub_account # returns a 404 below and requires the Canvas numeric ID parent_account_id = get_single_account(SDK_CONTEXT, id='sis_account_id:school:'+department.school_id).json()['id'] create_new_sub_account(request_ctx=SDK_CONTEXT, account_id=parent_account_id, account_name=department.name, sis_account_id=course_data.sis_account_id) if course_data.sis_account_id.startswith('coursegroup:'): # TLT-3878 Check to see if the course group exists in Canvas # If it does not, then create it try: get_single_account(request_ctx=SDK_CONTEXT, id='sis_account_id:%s' % course_data.sis_account_id) except CanvasAPIError: course_group_id = course_data.sis_account_id.replace('coursegroup:', '') course_group = CourseGroup.objects.get(course_group_id=course_group_id) logger.info("Course group does not exist for {}, creating one now".format(course_data.sis_account_id)) # It seems that using the sis_account_id:xxx in create_new_sub_account # returns a 404 below and requires the Canvas numeric ID parent_account_id = get_single_account(SDK_CONTEXT, id='sis_account_id:school:' + course_group.school_id).json()['id'] create_new_sub_account(request_ctx=SDK_CONTEXT, account_id=parent_account_id, account_name=course_group.name, sis_account_id=course_data.sis_account_id) # 3. Attempt to create a canvas course request_parameters = dict( request_ctx=SDK_CONTEXT, account_id='sis_account_id:%s' % course_data.sis_account_id, course_name=course_data.course_name, course_course_code=course_data.course_code, course_term_id='sis_term_id:%s' % course_data.sis_term_id, course_sis_course_id=sis_course_id, ) # If this was not part of a bulk job, attempt to get the default template for the given school if not bulk_job_id: try: template_id = get_default_template_for_school(course_data.school_code).template_id except NoTemplateExistsForSchool: # No template exists for the school, so no need to copy visibility settings pass # If creating from a template, get template course, so visibility settings # can be copied over to the new course if template_id: try: template_course = get_single_course_courses(SDK_CONTEXT, template_id, 'all_courses').json() # Update create course request parameters request_parameters.update({ 'course_is_public': template_course['is_public'], 'course_public_syllabus': template_course['public_syllabus'], }) except CanvasAPIError: logger.exception( 'Failed to retrieve template course %d for creation of site for course instance %s in account %s', template_id, sis_course_id, course_data.sis_account_id ) # Update the status to STATUS_SETUP_FAILED on failure to retrieve template course update_course_generation_workflow_state( sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_generation_job.id, bulk_job_id=bulk_job_id ) ex = CanvasCourseCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex try: new_course = create_new_course(**request_parameters).json() except CanvasAPIError as api_error: logger.exception( 'Error building request_parameters or executing create_new_course() ' 'SDK call for new Canvas course with request=%s:', request_parameters) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) # a 400 errors here means that the SIS id already exists in Canvas if api_error.status_code == 400: raise CanvasCourseAlreadyExistsError(msg_details=sis_course_id) ex = CanvasCourseCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex logger.info("created course object, ret=%s" % new_course) # 4. Save the canvas course id to the generation job course_generation_job.canvas_course_id = new_course['id'] try: course_generation_job.save(update_fields=['canvas_course_id']) except Exception as e: # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SaveCanvasCourseIdToCourseGenerationJobError( msg_details=(new_course['id'], course_generation_job.pk)) logging.exception(ex.display_text) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # 5. Save the canvas course id to the course instance course_data.canvas_course_id = new_course['id'] try: course_data.save(update_fields=['canvas_course_id']) except Exception as e: # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SaveCanvasCourseIdToCourseInstanceError( msg_details=(new_course['id'], course_data.pk)) logging.exception(ex.display_text) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # 6. Create course section after course creation try: request_parameters = dict(request_ctx=SDK_CONTEXT, course_id=new_course['id'], course_section_name=course_data.primary_section_name(), course_section_sis_section_id=sis_course_id) section = create_course_section(**request_parameters).json() logger.info("created section= %s" % section) except CanvasAPIError as e: logger.exception( 'Error building request_parameters or executing ' 'create_course_section() SDK call for new Canvas course id=%s with ' 'request=%s' % (new_course.get('id', '<no ID>'), request_parameters)) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) # send email in addition to showing error page to user ex = CanvasSectionCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # if this creation is part of a single course creation process return # the course along with the new job_id. The start_course_template_copy method will updated # the worng record if job id is no supplied. if course_job_id: return new_course, course_job_id return new_course
def create_canvas_course(sis_course_id, sis_user_id, bulk_job=None): """ This method creates a canvas course for the sis_course_id provided, initiated by the sis_user_id. The bulk_job_id would be passed in if it's invoked from a bulk feed process. """ # instantiate any variables required for method return or logger calls new_course = None section = None course_job_id = None # 1. Insert a CanvasCourseGenerationJob record on initiation with STATUS_SETUP status. This would help in # keeping track of the status of the various courses in the bulk job context as well as general reporting # if there's no bulk id, we need to create the CanvasCourseGenerationJob bulk_job_id = None template_id = None if bulk_job: bulk_job_id = bulk_job.id template_id = bulk_job.template_canvas_course_id try: course_generation_job = CanvasCourseGenerationJob.objects.filter( sis_course_id=sis_course_id, bulk_job_id=bulk_job_id).get() except Exception as e: ex = CourseGenerationJobNotFoundError(msg_details=(bulk_job_id, sis_course_id)) logger.exception(ex.display_text) raise ex else: try: logger.debug('Create content migration job tracking row...') course_generation_job = CanvasCourseGenerationJob.objects.create( sis_course_id=sis_course_id, created_by_user_id=sis_user_id, workflow_state=CanvasCourseGenerationJob.STATUS_SETUP, ) course_job_id = course_generation_job.pk logger.debug('Job row created: %s' % course_generation_job) except Exception as e: logger.exception('Error in inserting CanvasCourseGenerationJob record for ' 'with sis_course_id=%s: exception=%s' % (sis_course_id, e)) # send email in addition to showing error page to user ex = CourseGenerationJobCreationError(msg_details=sis_course_id) send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex try: # 2. fetch the course instance info course_data = get_course_data(sis_course_id) logger.info("\n obtained course info for ci=%s, acct_id=%s, course_name=%s, code=%s, term=%s, section_name=%s\n" % (course_data, course_data.sis_account_id, course_data.course_name, course_data.course_code, course_data.sis_term_id, course_data.primary_section_name())) except ObjectDoesNotExist as e: logger.error('ObjectDoesNotExist exception when fetching SIS data for course ' 'with sis_course_id=%s: exception=%s' % (sis_course_id, e)) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state( sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SISCourseDoesNotExistError(sis_course_id) # If the course is part of bulk job, do not send individual email. . if not bulk_job_id: msg = ex.display_text # TLT-393: send an email to support group, in addition to showing error page to user send_failure_msg_to_support(sis_course_id, sis_user_id, msg) raise ex # 3. Attempt to create a canvas course request_parameters = dict( request_ctx=SDK_CONTEXT, account_id='sis_account_id:%s' % course_data.sis_account_id, course_name=course_data.course_name, course_course_code=course_data.course_code, course_term_id='sis_term_id:%s' % course_data.sis_term_id, course_sis_course_id=sis_course_id, course_is_public_to_auth_users=course_data.shopping_active ) # If this was not part of a bulk job, attempt to get the default template for the given school if not bulk_job_id: try: template_id = get_default_template_for_school(course_data.school_code).template_id except NoTemplateExistsForSchool: # No template exists for the school, so no need to copy visibility settings pass # If creating from a template, get template course, so visibility settings # can be copied over to the new course if template_id: try: template_course = get_single_course_courses(SDK_CONTEXT, template_id, 'all_courses').json() is_public_to_auth_users = course_data.shopping_active or template_course['is_public_to_auth_users'] # Update create course request parameters request_parameters.update({ 'course_is_public': template_course['is_public'], 'course_public_syllabus': template_course['public_syllabus'], 'course_is_public_to_auth_users': is_public_to_auth_users }) except CanvasAPIError: logger.exception( 'Failed to retrieve template course %d for creation of site for course instance %s in account %s', template_id, sis_course_id, course_data.sis_account_id ) # Update the status to STATUS_SETUP_FAILED on failure to retrieve template course update_course_generation_workflow_state( sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_generation_job.id, bulk_job_id=bulk_job_id ) ex = CanvasCourseCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex try: new_course = create_new_course(**request_parameters).json() except CanvasAPIError as api_error: logger.exception( 'Error building request_parameters or executing create_new_course() ' 'SDK call for new Canvas course with request=%s:', request_parameters) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) # a 400 errors here means that the SIS id already exists in Canvas if api_error.status_code == 400: raise CanvasCourseAlreadyExistsError(msg_details=sis_course_id) ex = CanvasCourseCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex logger.info("created course object, ret=%s" % new_course) # 4. Save the canvas course id to the generation job course_generation_job.canvas_course_id = new_course['id'] try: course_generation_job.save(update_fields=['canvas_course_id']) except Exception as e: # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SaveCanvasCourseIdToCourseGenerationJobError( msg_details=(new_course['id'], course_generation_job.pk)) logging.exception(ex.display_text) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # 5. Save the canvas course id to the course instance course_data.canvas_course_id = new_course['id'] try: course_data.save(update_fields=['canvas_course_id']) except Exception as e: # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) ex = SaveCanvasCourseIdToCourseInstanceError( msg_details=(new_course['id'], course_data.pk)) logging.exception(ex.display_text) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # 6. Create course section after course creation try: request_parameters = dict(request_ctx=SDK_CONTEXT, course_id=new_course['id'], course_section_name=course_data.primary_section_name(), course_section_sis_section_id=sis_course_id) section = create_course_section(**request_parameters).json() logger.info("created section= %s" % section) except CanvasAPIError as e: logger.exception( 'Error building request_parameters or executing ' 'create_course_section() SDK call for new Canvas course id=%s with ' 'request=%s' % (new_course.get('id', '<no ID>'), request_parameters)) # Update the status to STATUS_SETUP_FAILED on any failures update_course_generation_workflow_state(sis_course_id, CanvasCourseGenerationJob.STATUS_SETUP_FAILED, course_job_id=course_job_id, bulk_job_id=bulk_job_id) # send email in addition to showing error page to user ex = CanvasSectionCreateError(msg_details=sis_course_id) if not bulk_job_id: send_failure_msg_to_support(sis_course_id, sis_user_id, ex.display_text) raise ex # if this creation is part of a single course creation process return # the course along with the new job_id. The start_course_template_copy method will updated # the worng record if job id is no supplied. if course_job_id: return new_course, course_job_id return new_course
def create_canvas_course_and_section(request): try: data = json.loads(request.body) account_id = 'sis_account_id:%s' % data['dept_id'] # not using .get() default because we want to fall back on course_code # if short_title is an empty string course_code = data.get('short_title', '').strip() or data['course_code'] course_instance_id = data['course_instance_id'] school = data['school'] section_id = data['section_id'] term_id = 'sis_term_id:%s' % data['term_id'] title = data['title'] except Exception: message = (u'Failed to extract canvas parameters from posted data; ' u'request body={}'.format(request.body)) logger.exception(message) return JsonResponse({'error': message}, status=400) request_parameters = dict( request_ctx=SDK_CONTEXT, account_id=account_id, course_course_code=course_code, course_name=title, course_sis_course_id=course_instance_id, course_term_id=term_id ) try: course_result = create_new_course(**request_parameters).json() except Exception as e: message = u'Error creating new course via SDK with request={}'.format( request_parameters) if isinstance(e, CanvasAPIError): message += u', SDK error details={}'.format(e) logger.exception(message) return JsonResponse({}, status=500) # create the canvas section section_result = {} request_parameters = {} try: # format section name similar to how it is handled in the bulk feed : # school + short title/course_code + section id section_name = u'{} {} {}'.format(school.upper(), course_code, section_id) request_parameters = dict( request_ctx=SDK_CONTEXT, course_id=course_result['id'], course_section_name=section_name, course_section_sis_section_id=course_instance_id) section_result = create_course_section(**request_parameters).json() except Exception as e: message = (u'Error creating section for new course via SDK with ' u'request={}'.format(request_parameters)) if isinstance(e, CanvasAPIError): message += u', SDK error details={}'.format(e) logger.exception(message) return JsonResponse(section_result, status=500) return JsonResponse(course_result, status=200)
def create_canvas_course_and_section(request): try: data = json.loads(request.body) is_blueprint = data['is_blueprint'] # If this is a blueprint course, create course at school level not in the ILE sub account account_id = 'sis_account_id:%s' % (data['school_id'] if is_blueprint else data['dept_id']) # not using .get() default because we want to fall back on course_code # if short_title is an empty string course_code = data.get('short_title', '').strip() or data['course_code'] course_instance_id = data['course_instance_id'] school = data['school'] section_id = data['section_id'] term_id = 'sis_term_id:%s' % data['term_id'] title = data['title'] except Exception: message = (u'Failed to extract canvas parameters from posted data; ' u'request body={}'.format(request.body)) logger.exception(message) return JsonResponse({'error': message}, status=400) request_parameters = dict(request_ctx=SDK_CONTEXT, account_id=account_id, course_course_code=course_code, course_name=title, course_sis_course_id=course_instance_id, course_term_id=term_id) try: course_result = create_new_course(**request_parameters).json() # If this course is meant to be a blueprint course, # the newly created course needs to have its blueprint field set to True if is_blueprint: update_parameters = dict(request_ctx=SDK_CONTEXT, id=course_result['id'], course_blueprint=True) try: update_course(**update_parameters).json() except: logger.exception( "Error creating blueprint course via update with request {}" .format(update_parameters)) return JsonResponse({}, status=500) except Exception as e: message = u'Error creating new course via SDK with request={}'.format( request_parameters) if isinstance(e, CanvasAPIError): message += u', SDK error details={}'.format(e) logger.exception(message) return JsonResponse({}, status=500) # create the canvas section section_result = {} request_parameters = {} try: # format section name similar to how it is handled in the bulk feed : # school + short title/course_code + section id section_name = u'{} {} {}'.format(school.upper(), course_code, section_id) request_parameters = dict( request_ctx=SDK_CONTEXT, course_id=course_result['id'], course_section_name=section_name, course_section_sis_section_id=course_instance_id) section_result = create_course_section(**request_parameters).json() except Exception as e: message = (u'Error creating section for new course via SDK with ' u'request={}'.format(request_parameters)) if isinstance(e, CanvasAPIError): message += u', SDK error details={}'.format(e) logger.exception(message) return JsonResponse(section_result, status=500) return JsonResponse(course_result, status=200)