示例#1
0
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.')
示例#2
0
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())
示例#3
0
 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()
示例#4
0
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.')
示例#5
0
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.')
示例#6
0
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,
    )
示例#7
0
 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),
     }
示例#8
0
    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)
示例#9
0
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())
示例#10
0
 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)
示例#11
0
    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
示例#12
0
    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