def update_course(course_id): course = DegreeProgressCourse.find_by_id(course_id) if course: params = request.get_json() accent_color = normalize_accent_color(get_param(params, 'accentColor')) grade = get_param(params, 'grade') grade = course.grade if grade is None else grade name = get_param(params, 'name') name = course.display_name if name is None else name if name is None: raise BadRequestError('name is required.') note = get_param(params, 'note') # Courses are mapped to degree_progress_unit_requirements value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter( None, value.split(','))) if isinstance(value, str) else value units = get_param(params, 'units') or None course = DegreeProgressCourse.update( accent_color=accent_color, course_id=course_id, grade=grade, name=name, note=note, unit_requirement_ids=unit_requirement_ids, units=units, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json()) else: raise ResourceNotFoundError('Course not found.')
def create_course(): params = request.get_json() accent_color = normalize_accent_color(get_param(params, 'accentColor')) degree_check_id = get_param(params, 'degreeCheckId') grade = get_param(params, 'grade') name = get_param(params, 'name') note = get_param(params, 'note') sid = get_param(params, 'sid') value = get_param(request.get_json(), 'unitRequirementIds') unit_requirement_ids = list(filter(None, value.split(','))) if isinstance( value, str) else value units = get_param(params, 'units') if 0 in map(lambda v: len(str(v).strip()) if v else 0, (name, sid)): raise BadRequestError('Missing one or more required parameters') course = DegreeProgressCourse.create( accent_color=accent_color, degree_check_id=degree_check_id, display_name=name, grade=grade, manually_created_at=datetime.now(), manually_created_by=current_user.get_id(), note=note, section_id=None, sid=sid, term_id=None, unit_requirement_ids=unit_requirement_ids or (), units=units, ) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json())
def delete(cls, category_id): for unit_requirement in DegreeProgressCategoryUnitRequirement.find_by_category_id( category_id): db.session.delete(unit_requirement) for course in DegreeProgressCourse.find_by_category_id(category_id): db.session.delete(course) std_commit() category = cls.query.filter_by(id=category_id).first() db.session.delete(category) std_commit()
def delete_course(course_id): course = DegreeProgressCourse.find_by_id(course_id) if course: # Verify that course is a copy matches = DegreeProgressCourse.get_courses( degree_check_id=course.degree_check_id, manually_created_at=course.manually_created_at, manually_created_by=course.manually_created_by, section_id=course.section_id, sid=course.sid, term_id=course.term_id, ) def _delete_course(c): category = DegreeProgressCategory.find_by_id(c.category_id) DegreeProgressCourseUnitRequirement.delete(c.id) DegreeProgressCourse.delete(c) if category and 'Placeholder' in category.category_type: DegreeProgressCategory.delete(category_id=category.id) matches = sorted(matches, key=lambda c: c.created_at) if matches[0].id == course.id: if course.manually_created_by: for match in matches: _delete_course(match) else: raise BadRequestError('Only copied courses can be deleted.') else: _delete_course(course) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify({'message': f'Course {course_id} deleted'}), 200 else: raise ResourceNotFoundError('Course not found.')
def assign_course(course_id): params = request.get_json() course = DegreeProgressCourse.find_by_id(course_id) if course: # Get existing category assignment. If it's a placeholder then delete it at end of transaction. previous_category = DegreeProgressCategory.find_by_id( course.category_id) if course.category_id else None category_id = get_param(params, 'categoryId') category = DegreeProgressCategory.find_by_id( category_id) if category_id else None if category: if category.template_id != course.degree_check_id: raise BadRequestError( 'The category and course do not belong to the same degree_check instance.' ) children = DegreeProgressCategory.find_by_parent_category_id( parent_category_id=category_id) if next((c for c in children if c.category_type == 'Subcategory'), None): raise BadRequestError( 'A course cannot be assigned to a category with a subcategory.' ) course = DegreeProgressCourse.assign(category_id=category_id, course_id=course.id) else: # When user un-assigns a course we delete all copies of that course,. for copy_of_course in DegreeProgressCourse.get_courses( degree_check_id=course.degree_check_id, manually_created_at=course.manually_created_at, manually_created_by=course.manually_created_by, section_id=course.section_id, sid=course.sid, term_id=course.term_id, ): if copy_of_course.id != course.id: DegreeProgressCourseUnitRequirement.delete( copy_of_course.id) category = DegreeProgressCategory.find_by_id( copy_of_course.category_id) if category and 'Placeholder' in category.category_type: DegreeProgressCategory.delete(category.id) DegreeProgressCourse.delete(copy_of_course) ignore = to_bool_or_none(get_param(params, 'ignore')) course = DegreeProgressCourse.unassign(course_id=course.id, ignore=ignore) # If previous assignment was a "placeholder" category then delete it. if previous_category and 'Placeholder' in previous_category.category_type: DegreeProgressCategory.delete(previous_category.id) # Update updated_at date of top-level record DegreeProgressTemplate.refresh_updated_at(course.degree_check_id, current_user.get_id()) return tolerant_jsonify(course.to_api_json()) else: raise ResourceNotFoundError('Course not found.')
def mock_degree_course(): marker = datetime.now().timestamp() sid = '11667051' degree_check = DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid), degree_name=f'Degree check of {coe_student_sid}', student_sid=sid, ) return DegreeProgressCourse.create( degree_check_id=degree_check.id, display_name=f'The Decline of Western Civilization ({marker})', grade='B+', section_id=datetime.utcfromtimestamp(0).microsecond, sid=sid, term_id=2218, units=4, )
def to_api_json(self): unit_requirements = [ m.unit_requirement.to_api_json() for m in (self.unit_requirements or []) ] return { 'id': self.id, 'accentColor': self.accent_color, 'categoryType': self.category_type, 'courses': [ c.to_api_json() for c in DegreeProgressCourse.find_by_category_id( category_id=self.id) ], 'createdAt': _isoformat(self.created_at), 'description': self.description, 'grade': self.grade, 'isRecommended': self.is_recommended, 'name': self.name, 'note': self.note, 'parentCategoryId': self.parent_category_id, 'position': self.position, 'templateId': self.template_id, 'unitsLower': self.course_units and self.course_units.lower, 'unitsUpper': self.course_units and self.course_units.upper, 'unitRequirements': sorted(unit_requirements, key=lambda r: r['name']), 'updatedAt': _isoformat(self.updated_at), }
def test_copy_course(self, client, fake_auth): """User can copy course and add it to a category.""" advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid) fake_auth.login(advisor.uid) sid = '11667051' degree_check = DegreeProgressTemplate.create( advisor_dept_codes=['COENG'], created_by=advisor.id, degree_name=f'Degree for {sid}', student_sid=sid, ) std_commit(allow_test_environment=True) degree_check_id = degree_check.id # Set up def _create_category(category_type='Category', parent_category_id=None): category = DegreeProgressCategory.create( category_type=category_type, name= f'{category_type} for {sid} ({datetime.now().timestamp()})', parent_category_id=parent_category_id, position=1, template_id=degree_check_id, ) return category category_1 = _create_category() category_2 = _create_category() std_commit(allow_test_environment=True) # Get sample course from list of unassigned courses api_json = _api_get_degree(client=client, degree_check_id=degree_check_id) course = api_json['courses']['unassigned'][-1] course_id = course['id'] section_id = course['sectionId'] copied_course_ids = [] def _copy_course(category_id, expected_status_code=200): course_copy = _api_copy_course( category_id=category_id, client=client, course_id=course_id, expected_status_code=expected_status_code, ) if expected_status_code == 200: copied_course_ids.append(course_copy['id']) return course_copy # Verify: user cannot copy an unassigned course. _copy_course(category_id=category_1.id, expected_status_code=400) # Verify: user cannot copy course to a category which already has the course. _api_assign_course(category_id=category_1.id, client=client, course_id=course_id) by_id = DegreeProgressCourse.find_by_id(course_id) assert by_id.category_id == category_1.id _copy_course(category_id=category_1.id, expected_status_code=400) subcategory = _create_category(category_type='Subcategory', parent_category_id=category_1.id) # Verify: user cannot copy course to a category which has subcategories. _copy_course(category_id=category_1.id, expected_status_code=400) # Verify we can create a copy of course in subcategory, thereby provisioning a new 'Course Requirement' child_count = len( DegreeProgressCategory.find_by_parent_category_id(subcategory.id)) copy_of_course = _copy_course(category_id=subcategory.id) children = DegreeProgressCategory.find_by_parent_category_id( subcategory.id) assert len(children) == child_count + 1 assert children[0].category_type == 'Placeholder: Course Copy' assert copy_of_course['categoryId'] == children[0].id # Assign the copied course to an actual Course Requirement and verify that "placeholder" category is deleted. course_requirement = _create_category( category_type='Course Requirement', parent_category_id=subcategory.id) placeholder_category_id = copy_of_course['categoryId'] _api_assign_course(category_id=course_requirement.id, client=client, course_id=copy_of_course['id']) assert DegreeProgressCategory.find_by_id( placeholder_category_id) is None # Finally, we create a copy for a separate category and expect success. copy_of_course = _copy_course(category_id=category_2.id) assert copy_of_course['id'] != course_id assert copy_of_course['sectionId'] == section_id # Verify 'isCopy' property per course degree_json = _api_get_degree(client=client, degree_check_id=degree_check_id) assigned_courses = degree_json['courses']['assigned'] unassigned_courses = degree_json['courses']['unassigned'] assert len(assigned_courses) assert len(unassigned_courses) # Expect no "copies" in the Unassigned set of courses. assert True not in [c['isCopy'] for c in unassigned_courses] for assigned_course in assigned_courses: course_id = assigned_course['id'] assert assigned_course['isCopy'] == (course_id in copied_course_ids)
def copy_course(): params = request.get_json() category_id = to_int_or_none(get_param(params, 'categoryId')) category = category_id and DegreeProgressCategory.find_by_id(category_id) if not category: raise ResourceNotFoundError('Category not found.') course_id = to_int_or_none(get_param(params, 'courseId')) course = DegreeProgressCourse.find_by_id(course_id) if not course: raise ResourceNotFoundError('Course not found.') sid = course.sid section_id = course.section_id term_id = course.term_id degree_check_id = category.template_id courses = DegreeProgressCourse.get_courses( degree_check_id=degree_check_id, manually_created_at=course.manually_created_at, manually_created_by=course.manually_created_by, section_id=section_id, sid=sid, term_id=term_id, ) if not len(courses): raise ResourceNotFoundError('Course not found.') elif len(courses) == 1 and not courses[0].category_id: raise BadRequestError( f'Course {courses[0].id} is unassigned. Use the /assign API instead.' ) else: if not _can_accept_course_requirements(category): raise BadRequestError( f'A \'Course Requirement\' cannot be added to a {category.category_type}.' ) # Verify that course is not already in the requested category/subcategory. for c in DegreeProgressCourse.find_by_category_id(category.id): if c.section_id == section_id and c.sid == sid and c.term_id: raise BadRequestError( f'Course already belongs to category {category.name}.') for child in DegreeProgressCategory.find_by_parent_category_id( category_id): if child.id == category_id: raise BadRequestError( f'Course already belongs to category {category.name}.') # Create a new course instance and a new 'Course Requirement'. course = courses[0] course = DegreeProgressCourse.create( accent_color=course.accent_color, degree_check_id=degree_check_id, display_name=course.display_name, grade=course.grade, manually_created_at=course.manually_created_at, manually_created_by=course.manually_created_by, section_id=section_id, sid=sid, term_id=term_id, unit_requirement_ids=[ u.unit_requirement_id for u in course.unit_requirements ], units=course.units, ) course_requirement = DegreeProgressCategory.create( category_type='Placeholder: Course Copy', name=course.display_name, position=category.position, template_id=degree_check_id, course_units_lower=course.units, course_units_upper=course.units, parent_category_id=category.id, unit_requirement_ids=[ u.unit_requirement_id for u in category.unit_requirements ], ) DegreeProgressCourse.assign(category_id=course_requirement.id, course_id=course.id) return tolerant_jsonify( DegreeProgressCourse.find_by_id(course.id).to_api_json())
def _delete_course(c): category = DegreeProgressCategory.find_by_id(c.category_id) DegreeProgressCourseUnitRequirement.delete(c.id) DegreeProgressCourse.delete(c) if category and 'Placeholder' in category.category_type: DegreeProgressCategory.delete(category_id=category.id)
def _get_partitioned_courses_json(self): assigned_courses = [] ignored_courses = [] unassigned_courses = [] degree_progress_courses = {} sid = self.student_sid # Sort courses by created_at (asc) so "copied" courses come after the primary assigned course. degree_courses = DegreeProgressCourse.find_by_sid(degree_check_id=self.id, sid=sid) for course in sorted(degree_courses, key=lambda c: c.created_at): key = f'{course.section_id}_{course.term_id}_{course.manually_created_at}_{course.manually_created_by}' if key not in degree_progress_courses: degree_progress_courses[key] = [] degree_progress_courses[key].append(course) def _categorize_course(course_, is_copy, units_original_value=None): api_json = { **course_.to_api_json(), **{ 'sis': { # If user edits degreeCheck.units then we alert the user of diff with original sis.units. 'units': units_original_value, }, }, 'isCopy': is_copy, } if api_json['categoryId']: assigned_courses.append(api_json) elif api_json['ignore']: ignored_courses.append(api_json) else: unassigned_courses.append(api_json) for section in _get_enrollment_sections(sid): grade = section['grade'] section_id = section['ccn'] term_id = section['termId'] units = section['units'] key = f'{section_id}_{term_id}_{None}_{None}' if key in degree_progress_courses: for idx, course in enumerate(degree_progress_courses.pop(key)): if grade != course.grade: course = DegreeProgressCourse.update_grade(course_id=course.id, grade=grade) _categorize_course( course_=course, is_copy=idx > 0, units_original_value=units, ) elif section.get('primary') and grade and units: course = DegreeProgressCourse.create( degree_check_id=self.id, display_name=section['displayName'], grade=grade, section_id=section_id, sid=sid, term_id=term_id, units=units, ) unassigned_courses.append({ **course.to_api_json(), **{ 'sis': { 'units': units, }, }, 'isCopy': False, }) for key in list(degree_progress_courses.keys()): for idx, course in enumerate(degree_progress_courses.pop(key)): _categorize_course(course_=course, is_copy=idx > 0) return assigned_courses, ignored_courses, unassigned_courses
def _get_partitioned_courses_json(self): assigned_courses = [] ignored_courses = [] unassigned_courses = [] degree_progress_courses = {} sid = self.student_sid # Sort courses by created_at (asc) so "copied" courses come after the primary assigned course. degree_courses = DegreeProgressCourse.find_by_sid(degree_check_id=self.id, sid=sid) for course in sorted(degree_courses, key=lambda c: c.created_at): key = f'{course.section_id}_{course.term_id}_{course.manually_created_at}_{course.manually_created_by}' if key not in degree_progress_courses: degree_progress_courses[key] = [] degree_progress_courses[key].append(course) enrollments = data_loch.get_enrollments_for_sid( sid=sid, latest_term_id=current_term_id(), ) def _organize_course_and_its_copies(course_key, units_original_value=None): for idx, course_ in enumerate(degree_progress_courses.pop(course_key)): api_json = { **course_.to_api_json(), **{ 'sis': { # If user edits degreeCheck.units then we alert the user of diff with original sis.units. 'units': units_original_value, }, }, 'isCopy': idx > 0, } if api_json['categoryId']: assigned_courses.append(api_json) elif api_json['ignore']: ignored_courses.append(api_json) else: unassigned_courses.append(api_json) for index, term in enumerate(merge_enrollment_terms(enrollments)): for enrollment in term.get('enrollments', []): for section in enrollment['sections']: section_id = section['ccn'] term_id = term['termId'] units = section['units'] key = f'{section_id}_{term_id}_{None}_{None}' if key in degree_progress_courses: _organize_course_and_its_copies(key, units_original_value=units) else: grade = section['grade'] if section.get('primary') and grade and units: course = DegreeProgressCourse.create( degree_check_id=self.id, display_name=enrollment['displayName'], grade=grade, section_id=section_id, sid=sid, term_id=term_id, units=units, ) unassigned_courses.append({ **course.to_api_json(), **{ 'sis': { 'units': units, }, }, 'isCopy': False, }) for key in list(degree_progress_courses.keys()): _organize_course_and_its_copies(key) return assigned_courses, ignored_courses, unassigned_courses