コード例 #1
0
    def test_create_nonunique_unit_requirements(self, db):
        """Requires unit requirements in a single template to have unique names."""
        coe_advisor_id = AuthorizedUser.get_id_per_uid(coe_advisor_uid)
        template = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=coe_advisor_id,
            degree_name='Celtic Studies BA 2021',
        )
        name = 'Celtic Requirements'
        unit_requirement = DegreeProgressUnitRequirement.create(
            created_by=coe_advisor_id,
            min_units=70,
            name=name,
            template_id=template.id,
        )
        assert unit_requirement

        with pytest.raises(IntegrityError):
            DegreeProgressUnitRequirement.create(
                created_by=coe_advisor_id,
                min_units=53,
                name=name,
                template_id=template.id,
            )
        db.session.rollback()
コード例 #2
0
    def test_create_unit_requirement(self):
        coe_advisor_id = AuthorizedUser.get_id_per_uid(coe_advisor_uid)
        template = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=coe_advisor_id,
            degree_name='Celtic Studies BA 2021',
        )
        min_units = 45
        name = 'Celtic Requirements'
        unit_requirement = DegreeProgressUnitRequirement.create(
            created_by=coe_advisor_id,
            min_units=min_units,
            name=name,
            template_id=template.id,
        )
        assert unit_requirement
        assert unit_requirement.__repr__(
        ) == f"""<DegreeProgressUnitRequirement id={unit_requirement.id},
                    name={name},
                    min_units={min_units},
                    template_id={template.id},
                    created_at={unit_requirement.created_at},
                    created_by={coe_advisor_id},
                    updated_at={unit_requirement.updated_at},
                    updated_by={coe_advisor_id}>"""

        assert template.unit_requirements
        assert len(template.unit_requirements) == 1
        assert unit_requirement.template
コード例 #3
0
def mock_degree_check():
    return 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='11667051',
    )
コード例 #4
0
    def test_update_template(self, client, fake_auth):
        """Authorized user can edit a template."""
        user = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
        fake_auth.login(user.uid)

        template = DegreeProgressTemplate.create(
            advisor_dept_codes=get_dept_codes(user),
            created_by=user.id,
            degree_name='Boogie Down Productions',
        )
        for index in (1, 2, 3):
            DegreeProgressUnitRequirement.create(
                created_by=user.id,
                min_units=index,
                name=f'Unit Requirement #{index}',
                template_id=template.id,
            )

        updated_name = 'KRS One'
        api_json = self._api_clone_template(client=client,
                                            name=updated_name,
                                            template_id=template.id)
        assert api_json['id'] != template.id
        assert api_json['name'] == updated_name
        assert len(api_json['unitRequirements']) == 3
コード例 #5
0
 def test_delete(self, client, fake_auth):
     """COE advisor can delete template."""
     fake_auth.login(coe_advisor_read_write_uid)
     user = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
     assert user.degree_progress_permission == 'read_write'
     template = DegreeProgressTemplate.create(['COENG'], user.id, f'Classical Civilizations, by {user.id}')
     assert client.delete(f'/api/degree/{template.id}').status_code == 200
     assert client.get(f'/api/degree/{template.id}').status_code == 404
コード例 #6
0
def mock_template():
    user = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
    marker = datetime.now().timestamp()
    return DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=user.id,
        degree_name=f'I am a mock template, made for a mock category ({marker})',
    )
コード例 #7
0
def mock_degree_check():
    user_id = AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid)
    parent_template = DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=user_id,
        degree_name='Zoology BS 2021',
    )
    degree_check = DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=user_id,
        degree_name='Zoology BS 2021',
        parent_template_id=parent_template.id,
        student_sid=coe_student_sid,
    )
    std_commit(allow_test_environment=True)
    yield degree_check
    # Avoid polluting other tests
    DegreeProgressTemplate.delete(degree_check.id)
    std_commit(allow_test_environment=True)
コード例 #8
0
def create_degree():
    params = request.get_json()
    name = get_param(params, 'name', None)
    validate_template_upsert(name=name)
    degree = DegreeProgressTemplate.create(
        advisor_dept_codes=dept_codes_where_advising(current_user),
        created_by=current_user.get_id(),
        degree_name=name,
    )
    return tolerant_jsonify(degree.to_api_json())
コード例 #9
0
def mock_degree_checks():
    user = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
    marker = datetime.now().timestamp()
    degree_checks = []
    for index in (1, 2, 3):
        parent_template = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=user.id,
            degree_name=f'I am a mock template ({marker}_{index})',
        )
        degree_checks.append(
            DegreeProgressTemplate.create(
                advisor_dept_codes=['COENG'],
                created_by=user.id,
                degree_name=f'I am a mock degree check ({marker}_{index})',
                student_sid=coe_student_sid,
                parent_template_id=parent_template.id,
            ), )
    std_commit(allow_test_environment=True)
    return degree_checks
コード例 #10
0
def mock_note():
    user = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
    template = DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=user.id,
        degree_name='I am a mock template, made for a mock note',
    )
    return DegreeProgressNote.upsert(
        body='A mock note.',
        template_id=template.id,
        updated_by=user.id,
    )
コード例 #11
0
def mock_unit_requirement():
    template = DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid),
        degree_name='Ursinology 2025',
    )
    return DegreeProgressUnitRequirement.create(
        created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid),
        min_units=16,
        name='Aureum Ursi',
        template_id=template.id,
    )
コード例 #12
0
def clone(template, created_by, name=None, sid=None):
    clone = DegreeProgressTemplate.create(
        advisor_dept_codes=dept_codes_where_advising(current_user),
        created_by=created_by,
        degree_name=name or template.degree_name,
        parent_template_id=template.id if sid else None,
        student_sid=sid,
    )
    unit_requirements_by_source_id = {}
    for unit_requirement in template.unit_requirements:
        source_id = unit_requirement.id
        unit_requirements_by_source_id[
            source_id] = DegreeProgressUnitRequirement.create(
                created_by=created_by,
                min_units=unit_requirement.min_units,
                name=unit_requirement.name,
                template_id=clone.id,
            )

    def _create_category(category_, parent_id):
        unit_requirement_ids = []
        for u in category_['unitRequirements']:
            source_id_ = u['id']
            cross_reference = unit_requirements_by_source_id[source_id_]
            unit_requirement_ids.append(cross_reference.id)
        return DegreeProgressCategory.create(
            category_type=category_['categoryType'],
            name=category_['name'],
            position=category_['position'],
            template_id=clone.id,
            course_units_lower=category_['unitsLower'],
            course_units_upper=category_['unitsUpper'],
            description=category_['description'],
            parent_category_id=parent_id,
            unit_requirement_ids=unit_requirement_ids,
        )

    for category in DegreeProgressCategory.get_categories(
            template_id=template.id):
        c = _create_category(category_=category, parent_id=None)
        for course in category['courseRequirements']:
            _create_category(category_=course, parent_id=c.id)
        for subcategory in category['subcategories']:
            s = _create_category(category_=subcategory, parent_id=c.id)
            for course in subcategory['courseRequirements']:
                _create_category(category_=course, parent_id=s.id)

    # TODO: Unit requirements?
    return DegreeProgressTemplate.find_by_id(clone.id)
コード例 #13
0
 def test_assign_and_unassign_course(self, client, fake_auth):
     """User can assign and unassign a course."""
     advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
     fake_auth.login(advisor.uid)
     sid = '11667051'
     # Set up
     degree_check = DegreeProgressTemplate.create(
         advisor_dept_codes=['COENG'],
         created_by=advisor.id,
         degree_name=f'Degree check for {sid}',
         student_sid=sid,
     )
     original_updated_at = degree_check.updated_at
     category = DegreeProgressCategory.create(
         category_type='Category',
         name=f'Category for {sid}',
         position=1,
         template_id=degree_check.id,
     )
     std_commit(allow_test_environment=True)
     # Assign
     api_json = _api_get_degree(client, degree_check_id=degree_check.id)
     course_id = api_json['courses']['unassigned'][-1]['id']
     course = _api_assign_course(category_id=category.id,
                                 client=client,
                                 course_id=course_id)
     # Verify assignment
     api_json = _api_get_degree(client, degree_check_id=degree_check.id)
     assert course['categoryId'] == category.id
     assert course_id in [c['id'] for c in api_json['courses']['assigned']]
     assert course_id not in [
         c['id'] for c in api_json['courses']['unassigned']
     ]
     # Unassign
     _api_assign_course(category_id=None,
                        client=client,
                        course_id=course_id)
     api_json = _api_get_degree(client, degree_check_id=degree_check.id)
     assert course_id not in [
         c['id'] for c in api_json['courses']['assigned']
     ]
     assert course_id in [
         c['id'] for c in api_json['courses']['unassigned']
     ]
     # Verify update of updated_at
     assert DegreeProgressTemplate.find_by_id(
         degree_check.id).updated_at != original_updated_at
コード例 #14
0
    def test_illegal_assign(self, client, fake_auth):
        """A course cannot be assigned to a category with a subcategory."""
        advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
        fake_auth.login(advisor.uid)
        sid = '11667051'
        # Set up
        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)

        category = self._api_create_category(client, degree_check.id)
        subcategory = self._api_create_category(
            client,
            degree_check.id,
            category_type='Subcategory',
            parent_category_id=category['id'],
        )
        campus_requirements_category = self._api_create_category(
            client, degree_check.id, category_type='Campus Requirements')
        campus_requirements = DegreeProgressCategory.find_by_parent_category_id(
            campus_requirements_category['id'])

        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        course_id = api_json['courses']['unassigned'][-1]['id']
        # Expect failure
        _api_assign_course(category_id=category['id'],
                           client=client,
                           course_id=course_id,
                           expected_status_code=400)
        _api_assign_course(category_id=campus_requirements_category['id'],
                           client=client,
                           course_id=course_id,
                           expected_status_code=400)
        _api_assign_course(category_id=campus_requirements[0].id,
                           client=client,
                           course_id=course_id,
                           expected_status_code=400)
        # Expect success
        _api_assign_course(category_id=subcategory['id'],
                           client=client,
                           course_id=course_id)
コード例 #15
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,
    )
コード例 #16
0
    def test_unassigned_courses(self, client, fake_auth):
        """Authorized user can un-assign a course."""
        advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
        fake_auth.login(advisor.uid)
        # Set up
        sid = '11667051'
        degree_check = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=advisor.id,
            degree_name=f'Degree check for {sid}',
            student_sid=sid,
        )
        category = DegreeProgressCategory.create(
            category_type='Category',
            name=f'Category for {sid}',
            position=1,
            template_id=degree_check.id,
        )
        std_commit(allow_test_environment=True)
        # Fetch
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        assigned_courses = api_json['courses']['assigned']
        unassigned_courses = api_json['courses']['unassigned']
        assert len(unassigned_courses)
        assigned_course_count = len(assigned_courses)
        unassigned_course_count = len(unassigned_courses)
        # Assign
        unassigned_course = unassigned_courses[-1]
        _api_assign_course(client=client,
                           category_id=category.id,
                           course_id=unassigned_course['id'])
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        # Verify
        assigned_courses = api_json['courses']['assigned']
        assert len(assigned_courses) == assigned_course_count + 1
        assert next((c for c in unassigned_courses
                     if c['sectionId'] == unassigned_course['sectionId']),
                    None)

        unassigned_courses = api_json['courses']['unassigned']
        assert len(unassigned_courses) == unassigned_course_count - 1
        assert next((c for c in unassigned_courses
                     if c['sectionId'] == unassigned_course['sectionId']),
                    None) is None
コード例 #17
0
    def test_ignored_courses(self, client, fake_auth):
        """Authorized user can ignore a course."""
        advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
        fake_auth.login(advisor.uid)
        # Set up
        sid = '11667051'
        degree_check = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=advisor.id,
            degree_name=f'Degree check for {sid}',
            student_sid=sid,
        )
        std_commit(allow_test_environment=True)
        # Fetch
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        unassigned_courses = api_json['courses']['unassigned']
        ignored_courses = api_json['courses']['ignored']
        ignored_courses_count = len(ignored_courses)
        # Ignore
        ignore_this_course = unassigned_courses[-1]
        _api_assign_course(
            client=client,
            category_id=None,
            course_id=ignore_this_course['id'],
            ignore=True,
        )
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        # Verify
        unassigned_courses = api_json['courses']['unassigned']
        assert next((c for c in unassigned_courses
                     if c['sectionId'] == ignore_this_course['sectionId']),
                    None) is None

        ignored_courses = api_json['courses']['ignored']
        assert len(ignored_courses) == ignored_courses_count + 1
        assert next((c for c in ignored_courses
                     if c['sectionId'] == ignore_this_course['sectionId']),
                    None)
コード例 #18
0
    def test_assign_and_unassign_course(self, app, client, fake_auth):
        """User can assign and unassign a course."""
        advisor = AuthorizedUser.find_by_uid(coe_advisor_read_write_uid)
        fake_auth.login(advisor.uid)
        sid = '11667051'
        # Set up
        degree_check = DegreeProgressTemplate.create(
            advisor_dept_codes=['COENG'],
            created_by=advisor.id,
            degree_name=f'Degree check for {sid}',
            student_sid=sid,
        )
        original_updated_at = degree_check.updated_at
        category = DegreeProgressCategory.create(
            category_type='Category',
            name=f'Category for {sid}',
            position=1,
            template_id=degree_check.id,
        )
        std_commit(allow_test_environment=True)
        # Assign
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        course_id = api_json['courses']['unassigned'][-1]['id']
        course = _api_assign_course(category_id=category.id,
                                    client=client,
                                    course_id=course_id)
        original_grade = course['grade']
        assert original_grade == 'P'

        # Verify assignment
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        assert course['categoryId'] == category.id
        assert course_id in [c['id'] for c in api_json['courses']['assigned']]
        assert course_id not in [
            c['id'] for c in api_json['courses']['unassigned']
        ]

        # Modify grade in data lake and verify that assigned course receives the update
        modified_grade = 'C+'
        _change_grade_in_data_loch(
            app=app,
            grade=modified_grade,
            section_id=course['sectionId'],
            sid=sid,
            term_id=course['termId'],
        )
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        assigned_course = api_json['courses']['assigned'][0]
        assert assigned_course['id'] == course_id
        assert assigned_course['grade'] == modified_grade
        _change_grade_in_data_loch(
            app=app,
            grade=original_grade,
            section_id=course['sectionId'],
            sid=sid,
            term_id=course['termId'],
        )

        # Unassign
        _api_assign_course(category_id=None,
                           client=client,
                           course_id=course_id)
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        assert course_id not in [
            c['id'] for c in api_json['courses']['assigned']
        ]
        unassigned_course = api_json['courses']['unassigned'][-1]
        assert unassigned_course['id'] == course_id
        assert unassigned_course['grade'] == original_grade
        # Verify update of updated_at
        assert DegreeProgressTemplate.find_by_id(
            degree_check.id).updated_at != original_updated_at
コード例 #19
0
def mock_template():
    return DegreeProgressTemplate.create(
        advisor_dept_codes=['COENG'],
        created_by=AuthorizedUser.get_id_per_uid(coe_advisor_read_write_uid),
        degree_name='Zoology BS 2021',
    )
コード例 #20
0
    def test_delete(self, client, fake_auth):
        """Advisor can delete course."""
        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,
        )
        api_json = _api_get_degree(client, degree_check_id=degree_check.id)
        course_id = api_json['courses']['unassigned'][-1]['id']
        categories = []
        for index in (1, 2, 3):
            categories.append(
                self._api_create_category(
                    category_type='Category',
                    client=client,
                    name=f'Category {index}',
                    position=index,
                    template_id=degree_check.id,
                ), )
        # Category #2 gets a child 'Course Requirement'
        course_requirement = self._api_create_category(
            category_type='Course Requirement',
            client=client,
            name='Course Requirement',
            parent_category_id=categories[1]['id'],
            position=categories[1]['position'],
            template_id=degree_check.id,
        )
        # Assign course to Category #1
        course = _api_assign_course(
            category_id=categories[0]['id'],
            client=client,
            course_id=course_id,
        )
        # Copy course to Category #2 and then assign it to the 'Course Requirement'
        course_id = course['id']
        course_copy_1 = _api_copy_course(
            category_id=categories[1]['id'],
            client=client,
            course_id=course_id,
        )
        placeholder_category_id = course_copy_1['categoryId']
        course_copy_1 = _api_assign_course(
            category_id=course_requirement['id'],
            client=client,
            course_id=course_copy_1['id'],
        )
        # Placeholder category is auto-deleted during re-assignment
        assert not DegreeProgressCategory.find_by_id(placeholder_category_id)
        # Delete the course_copy_1 and expect the underlying 'Course Requirement' to survive
        assert client.delete(
            f"/api/degree/course/{course_copy_1['id']}").status_code == 200

        # Copy course to Category #3 and then delete. Expect removal of course and its 'Placeholder' category.
        course_copy_2 = _api_copy_course(
            category_id=categories[2]['id'],
            client=client,
            course_id=course_id,
        )
        placeholder_category_id = course_copy_2['categoryId']
        assert 'Placeholder' in DegreeProgressCategory.find_by_id(
            placeholder_category_id).category_type
        assert client.delete(
            f"/api/degree/course/{course_copy_2['id']}").status_code == 200
        assert not DegreeProgressCategory.find_by_id(placeholder_category_id)
コード例 #21
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)