Exemplo n.º 1
0
    def test_reviewer_collection(self):
        c = ReviewerCollection()

        c[201] = null_tuple(Reviewer)._replace(id=201,
                                               person_id=2001,
                                               role=BaseReviewerRole.TECH)
        c[202] = null_tuple(Reviewer)._replace(id=202,
                                               person_id=2002,
                                               role=BaseReviewerRole.EXTERNAL)

        self.assertFalse(c.has_person(9999))

        self.assertTrue(c.has_person(2001))

        self.assertFalse(c.has_person(2001, roles=[BaseReviewerRole.EXTERNAL]))

        self.assertTrue(c.has_person(2001, roles=[BaseReviewerRole.TECH]))

        self.assertEqual(c.person_id_by_role(BaseReviewerRole.EXTERNAL),
                         [2002])

        self.assertFalse(c.has_role(BaseReviewerRole.FEEDBACK))

        self.assertTrue(c.has_role(BaseReviewerRole.EXTERNAL))

        self.assertEqual(c.role_by_person_id(2001), [BaseReviewerRole.TECH])

        self.assertEqual(c.role_by_person_id(2002),
                         [BaseReviewerRole.EXTERNAL])

        reviewers = c.values_by_role(BaseReviewerRole.EXTERNAL)
        self.assertIsInstance(reviewers, list)
        self.assertEqual(len(reviewers), 1)
        self.assertIsInstance(reviewers[0], Reviewer)
        self.assertEqual(reviewers[0].id, 202)
Exemplo n.º 2
0
    def test_call_collection(self):
        c = CallCollection()

        c[1] = null_tuple(Call)._replace(id=1,
                                         queue_id=11,
                                         state=CallState.OPEN)
        c[2] = null_tuple(Call)._replace(id=2,
                                         queue_id=12,
                                         state=CallState.OPEN)
        c[3] = null_tuple(Call)._replace(id=3,
                                         queue_id=13,
                                         state=CallState.UNOPENED)
        c[4] = null_tuple(Call)._replace(id=4,
                                         queue_id=13,
                                         state=CallState.CLOSED)

        self.assertEqual(
            [x.id for x in c.values_matching(state=CallState.OPEN)], [1, 2])

        self.assertEqual(
            [x.id for x in c.values_matching(state=CallState.UNOPENED)], [3])

        self.assertEqual(
            [x.id for x in c.values_matching(state=CallState.CLOSED)], [4])

        self.assertEqual([x.id for x in c.values_matching(queue_id=11)], [1])

        self.assertEqual([x.id for x in c.values_matching(queue_id=12)], [2])

        self.assertEqual([x.id for x in c.values_matching(queue_id=13)],
                         [3, 4])

        self.assertEqual([x.id for x in c.values_matching(queue_id=(11, 12))],
                         [1, 2])
Exemplo n.º 3
0
    def _test_affiliation_assignment(self, title, affiliations, pi, cois,
                                     ref_assignment):
        view = JCMT(1)

        # Prepare member collection.
        members = MemberCollection()

        i = 0

        if pi is not None:
            i += 1
            members[i] = null_tuple(Member)._replace(id=i,
                                                     affiliation_id=pi,
                                                     pi=True)

        for coi in cois:
            i += 1
            members[i] = null_tuple(Member)._replace(id=i,
                                                     affiliation_id=coi,
                                                     pi=False)

        # Compute assignment, make set of affiliations and check total.
        assignment = view.calculate_affiliation_assignment(
            db=None, members=members, affiliations=affiliations)

        assigned_affiliations = set(assignment.keys())
        assigned_total = sum(assignment.values())

        self.assertAlmostEqual(
            assigned_total,
            1.0,
            msg='{}: total assignment not equal to 1'.format(title))

        # Check that the assignmenets match those specified.
        for (aff_id, aff_frac) in ref_assignment.items():
            self.assertIn(aff_id,
                          assignment,
                          msg='{}: affiliation {} missing'.format(
                              title, aff_id))
            self.assertAlmostEqual(
                assignment[aff_id],
                aff_frac,
                msg='{}: affiliation {}: {} (should be {})'.format(
                    title, aff_id, assignment[aff_id], aff_frac),
                places=5)
            assigned_affiliations.remove(aff_id)

        # There should be no other un-matched assignments.
        self.assertFalse(assigned_affiliations,
                         msg='{}: extra affiliations present: {!r}'.format(
                             title, list(assigned_affiliations)))
Exemplo n.º 4
0
    def test_feedback_extra(self):
        # Set up test proposal with a JCMT allocation.
        types = self.view.get_call_types()
        proposal_id = self._create_test_proposal('20A', 'P', types.STANDARD)
        proposal = self.db.get_proposal(
            facility_id=None, proposal_id=proposal_id)

        rc = JCMTRequestCollection()
        rc[1] = null_tuple(JCMTRequest)._replace(
            instrument=JCMTInstrument.HARP, ancillary=JCMTAncillary.NONE,
            weather=JCMTWeather.BAND5, time=4.5)

        self.db.sync_jcmt_proposal_allocation(proposal_id, rc)

        # Test the get_feedback_extra method.
        extra = self.view.get_feedback_extra(self.db, proposal)

        self.assertIsInstance(extra, dict)

        self.assertEqual(set(extra.keys()), set(('jcmt_allocation',)))

        alloc = extra['jcmt_allocation']

        self.assertIsInstance(alloc, list)

        self.assertEqual(len(alloc), 1)

        a = alloc[0]

        self.assertEqual(a.instrument, 'HARP')
        self.assertEqual(a.weather, 'Band 5')
        self.assertEqual(a.time, 4.5)
        self.assertIsNone(a.ancillary)
Exemplo n.º 5
0
    def test_proposal_figure_collection(self):
        fc = ProposalFigureCollection()

        fc[1001] = null_tuple(ProposalFigureInfo)._replace(
            id=1001, role=BaseTextRole.TECHNICAL_CASE)
        fc[1002] = null_tuple(ProposalFigureInfo)._replace(
            id=1002, role=BaseTextRole.TECHNICAL_CASE)
        fc[1003] = null_tuple(ProposalFigureInfo)._replace(
            id=1003, role=BaseTextRole.SCIENCE_CASE)
        fc[1004] = null_tuple(ProposalFigureInfo)._replace(
            id=1004, role=BaseTextRole.SCIENCE_CASE)

        tech = set(
            (x.id for x in fc.values_by_role(BaseTextRole.TECHNICAL_CASE)))
        sci = set((x.id for x in fc.values_by_role(BaseTextRole.SCIENCE_CASE)))

        self.assertEqual(tech, set((1001, 1002)))
        self.assertEqual(sci, set((1003, 1004)))
Exemplo n.º 6
0
    def test_group_member_collection(self):
        c = GroupMemberCollection()

        c[101] = null_tuple(GroupMember)._replace(id=101,
                                                  group_type=GroupType.CTTEE,
                                                  facility_id=1)
        c[102] = null_tuple(GroupMember)._replace(id=102,
                                                  group_type=GroupType.TECH,
                                                  facility_id=1)
        c[103] = null_tuple(GroupMember)._replace(id=103,
                                                  group_type=GroupType.TECH,
                                                  facility_id=1)
        c[104] = null_tuple(GroupMember)._replace(id=104,
                                                  group_type=GroupType.COORD,
                                                  facility_id=2)

        self.assertEqual(
            sorted([x.id for x in c.values_by_group_type(GroupType.CTTEE)]),
            [101])

        self.assertEqual(
            sorted([x.id for x in c.values_by_group_type(GroupType.TECH)]),
            [102, 103])

        self.assertEqual(
            sorted([x.id for x in c.values_by_group_type(GroupType.COORD)]),
            [104])

        self.assertTrue(c.has_entry(group_type=GroupType.CTTEE))
        self.assertTrue(
            c.has_entry(group_type=(GroupType.TECH, GroupType.COORD)))
        self.assertFalse(c.has_entry(group_type=999))

        self.assertTrue(c.has_entry(facility_id=1))
        self.assertTrue(c.has_entry(facility_id=2))
        self.assertFalse(c.has_entry(facility_id=3))

        self.assertTrue(c.has_entry(group_type=GroupType.CTTEE, facility_id=1))
        self.assertFalse(c.has_entry(group_type=GroupType.CTTEE,
                                     facility_id=2))

        self.assertTrue(c.has_entry(group_type=GroupType.COORD, facility_id=2))
        self.assertFalse(c.has_entry(group_type=GroupType.COORD,
                                     facility_id=1))
Exemplo n.º 7
0
    def test_call_preamble_collection(self):
        c = CallPreambleCollection()

        c[101] = null_tuple(CallPreamble)._replace(id=101, type=1)
        c[102] = null_tuple(CallPreamble)._replace(id=102, type=2)

        v = c.get_type(1)
        self.assertIsInstance(v, CallPreamble)
        self.assertEqual(v.id, 101)

        with self.assertRaises(NoSuchValue):
            c.get_type(999)

        v = c.get_type(2, default=None)
        self.assertIsInstance(v, CallPreamble)
        self.assertEqual(v.id, 102)

        v = c.get_type(999, default=None)
        self.assertIsNone(v)
Exemplo n.º 8
0
    def test_email_collection(self):
        c = EmailCollection()

        c[1] = null_tuple(Email)._replace(address='a@b', primary=False)

        with self.assertRaisesRegexp(UserError, 'There is no primary'):
            c.validate()

        c[2] = null_tuple(Email)._replace(address='c@d', primary=True)

        c.validate()

        c[3] = null_tuple(Email)._replace(address='e@f', primary=True)

        with self.assertRaisesRegexp(UserError, 'more than one primary'):
            c.validate()

        c[3] = null_tuple(Email)._replace(address='a@b', primary=False)

        with self.assertRaisesRegexp(UserError, 'appears more than once'):
            c.validate()
Exemplo n.º 9
0
    def test_member_collection(self):
        c = MemberCollection()

        c[101] = null_tuple(Member)._replace(id=101,
                                             person_id=9001,
                                             pi=False,
                                             person_name='Person One')

        with self.assertRaises(KeyError):
            c.get_pi()

        self.assertIsNone(c.get_pi(default=None))

        c[102] = null_tuple(Member)._replace(id=102,
                                             person_id=9002,
                                             pi=True,
                                             person_name='Person Two')

        c[103] = null_tuple(Member)._replace(id=103,
                                             person_id=9003,
                                             pi=False,
                                             person_name='Person Three')

        result = c.get_pi()
        self.assertEqual(result.person_id, 9002)

        result = c.get_person(9003)
        self.assertEqual(result.id, 103)

        # hedwig.view.auth.for_proposal relies on this exception being
        # raised when the current user isn't a member of the proposal.
        with self.assertRaises(KeyError):
            c.get_person(999999)

        self.assertTrue(c.has_person(9001))
        self.assertFalse(c.has_person(999999))
Exemplo n.º 10
0
    def test_proposal_collection(self):
        c = ProposalCollection()

        c[101] = null_tuple(Proposal)._replace(id=101, facility_id=1)
        c[102] = null_tuple(Proposal)._replace(id=102, facility_id=1)
        c[103] = null_tuple(Proposal)._replace(id=103, facility_id=1)
        c[201] = null_tuple(Proposal)._replace(id=201, facility_id=2)
        c[202] = null_tuple(Proposal)._replace(id=202, facility_id=2)
        c[203] = null_tuple(Proposal)._replace(id=203, facility_id=2)

        fl = list(c.values_by_facility(facility_id=1))
        self.assertEqual([x.id for x in fl], [101, 102, 103])

        fl = list(c.values_by_facility(facility_id=2))
        self.assertEqual([x.id for x in fl], [201, 202, 203])

        fl = list(c.values_by_facility(facility_id=3))
        self.assertEqual(fl, [])
Exemplo n.º 11
0
    def test_jcmt_review(self):
        """
        Test methods associated with the "jcmt_review" table.
        """

        role_class = JCMTReviewerRole

        proposal_id = self._create_test_proposal()
        person_id = self.db.add_person('Test Reviewer')

        reviewer_id = self.db.add_reviewer(
            role_class, proposal_id, person_id, role_class.CTTEE_PRIMARY)

        self.assertIsInstance(reviewer_id, int)

        with self.assertRaises(NoSuchRecord):
            self.db.get_jcmt_review(reviewer_id)

        with self.assertRaisesRegexp(
                ConsistencyError, 'JCMT review does not exist'):
            self.db.set_jcmt_review(
                role_class, reviewer_id,
                review=null_tuple(JCMTReview)._replace(
                    expertise=JCMTReviewerExpertise.INTERMEDIATE),
                is_update=True)

        with self.assertRaisesRegexp(
                UserError, 'expertise level not recognised'):
            self.db.set_jcmt_review(
                role_class, reviewer_id,
                review=null_tuple(JCMTReview)._replace(expertise=999),
                is_update=False)

        with self.assertRaisesRegexp(
                Error, 'expertise should be specified'):
            self.db.set_jcmt_review(
                role_class, reviewer_id,
                review=null_tuple(JCMTReview)._replace(expertise=None),
                is_update=False)

        self.db.set_jcmt_review(
            role_class, reviewer_id,
            review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.INTERMEDIATE),
            is_update=False)

        jcmt_review = self.db.get_jcmt_review(reviewer_id)

        self.assertIsInstance(jcmt_review, JCMTReview)
        self.assertEqual(jcmt_review.expertise,
                         JCMTReviewerExpertise.INTERMEDIATE)

        with self.assertRaisesRegexp(
                ConsistencyError, 'JCMT review already exist'):
            self.db.set_jcmt_review(
                role_class, reviewer_id,
                review=null_tuple(JCMTReview)._replace(
                    expertise=JCMTReviewerExpertise.INTERMEDIATE),
                is_update=False)

        self.db.set_jcmt_review(
            role_class, reviewer_id,
            review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.EXPERT),
            is_update=True)

        jcmt_review = self.db.get_jcmt_review(reviewer_id)

        self.assertIsNotNone(jcmt_review)
        self.assertIsInstance(jcmt_review, JCMTReview)
        self.assertEqual(jcmt_review.expertise,
                         JCMTReviewerExpertise.EXPERT)

        # Add a second review and try a multiple-reviewer search.
        reviewer_id_2 = self.db.add_reviewer(
            role_class, proposal_id, person_id, role_class.CTTEE_SECONDARY)

        self.assertIsInstance(reviewer_id_2, int)

        self.db.set_jcmt_review(
            role_class, reviewer_id_2,
            review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.INTERMEDIATE),
            is_update=False)

        result = self.db.search_jcmt_review(
            reviewer_id=[reviewer_id, reviewer_id_2])

        self.assertIsInstance(result, ResultCollection)

        self.assertEqual(set(result.keys()), set((reviewer_id, reviewer_id_2)))
Exemplo n.º 12
0
 def make_review(id, role, rating, weight):
     return null_tuple(Reviewer)._replace(
         id=101, role=role, review_rating=rating,
         review_weight=weight, review_state=ReviewState.DONE)
Exemplo n.º 13
0
    def test_reviewer_collection_rating(self):
        c = ReviewerCollection()

        rr = BaseReviewerRole

        def rwf_inc_unweighted(reviewer):
            if reviewer.review_weight is None:
                if rr.get_info(reviewer.role).weight:
                    return (None, None)
                return (reviewer.review_rating, 1.0)
            return (reviewer.review_rating, reviewer.review_weight / 100.0)

        def rwf_exc_unweighted(reviewer):
            if reviewer.review_weight is None:
                return (None, None)
            return (reviewer.review_rating, reviewer.review_weight / 100.0)

        rating = c.get_overall_rating(rwf_inc_unweighted, with_std_dev=False)
        self.assertIsNone(rating)
        rating = c.get_overall_rating(rwf_inc_unweighted, with_std_dev=True)
        self.assertIsInstance(rating, tuple)
        self.assertEqual(len(rating), 2)
        self.assertIsNone(rating[0])
        self.assertIsNone(rating[1])

        # Add some simple review ratings.
        rs = [
            dict(role=rr.TECH),
            dict(role=rr.EXTERNAL),
            dict(role=rr.EXTERNAL, review_rating=20),
            dict(role=rr.CTTEE_PRIMARY),
            dict(role=rr.CTTEE_PRIMARY, review_rating=10),
            dict(role=rr.CTTEE_PRIMARY, review_rating=80, review_weight=100),
        ]

        for (r, n) in zip(rs, itertools.count(100)):
            c[n] = null_tuple(Reviewer)._replace(review_state=ReviewState.DONE,
                                                 **r)

        self.assertEqual(
            c.get_overall_rating(rwf_inc_unweighted, with_std_dev=False), 50)
        self.assertEqual(
            c.get_overall_rating(rwf_exc_unweighted, with_std_dev=False), 80)

        # Repeat test above, including calculation of standard deviation.
        rating = c.get_overall_rating(rwf_exc_unweighted, with_std_dev=True)
        self.assertIsInstance(rating, tuple)
        self.assertEqual(len(rating), 2)
        self.assertEqual(rating[0], 80.0)
        self.assertEqual(rating[1], 0.0)

        (rating, std_dev) = c.get_overall_rating(rwf_inc_unweighted,
                                                 with_std_dev=True)
        self.assertEqual(rating, 50.0)
        self.assertEqual(std_dev, 30.0)

        # Add some more reviews with non-100% weights.
        rs = [
            dict(role=rr.CTTEE_SECONDARY, review_rating=20, review_weight=50),
            dict(role=rr.CTTEE_SECONDARY, review_rating=40, review_weight=50),
        ]

        for (r, n) in zip(rs, itertools.count(200)):
            c[n] = null_tuple(Reviewer)._replace(review_state=ReviewState.DONE,
                                                 **r)

        self.assertEqual(
            c.get_overall_rating(rwf_exc_unweighted, with_std_dev=False), 55)

        (rating, std_dev) = c.get_overall_rating(rwf_exc_unweighted,
                                                 with_std_dev=True)
        self.assertEqual(rating, 55.0)
        self.assertAlmostEqual(std_dev, 25.981, places=3)
Exemplo n.º 14
0
 def make_review(id, role, rating, expertise):
     return null_tuple(Reviewer)._replace(
         id=101, role=role, review_rating=rating,
         review_state=ReviewState.DONE,
         review_extra=null_tuple(JCMTReview)._replace(
             expertise=expertise))
Exemplo n.º 15
0
    def test_jcmt_review(self):
        """
        Test methods associated with the "jcmt_review" table.
        """

        role_class = JCMTReviewerRole

        proposal_id = self._create_test_proposal()
        person_id = self.db.add_person('Test Reviewer')

        reviewer_id = self.db.add_reviewer(role_class, proposal_id, person_id,
                                           role_class.CTTEE_PRIMARY)

        self.assertIsInstance(reviewer_id, int)

        with self.assertRaises(NoSuchRecord):
            self.db.get_jcmt_review(reviewer_id)

        with self.assertRaisesRegexp(ConsistencyError,
                                     'JCMT review does not exist'):
            self.db.set_jcmt_review(
                role_class,
                reviewer_id,
                review=null_tuple(JCMTReview)._replace(
                    expertise=JCMTReviewerExpertise.INTERMEDIATE),
                is_update=True)

        with self.assertRaisesRegexp(UserError,
                                     'expertise level not recognised'):
            self.db.set_jcmt_review(
                role_class,
                reviewer_id,
                review=null_tuple(JCMTReview)._replace(expertise=999),
                is_update=False)

        with self.assertRaisesRegexp(Error, 'expertise should be specified'):
            self.db.set_jcmt_review(
                role_class,
                reviewer_id,
                review=null_tuple(JCMTReview)._replace(expertise=None),
                is_update=False)

        self.db.set_jcmt_review(
            role_class,
            reviewer_id,
            review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.INTERMEDIATE),
            is_update=False)

        jcmt_review = self.db.get_jcmt_review(reviewer_id)

        self.assertIsInstance(jcmt_review, JCMTReview)
        self.assertEqual(jcmt_review.expertise,
                         JCMTReviewerExpertise.INTERMEDIATE)

        with self.assertRaisesRegexp(ConsistencyError,
                                     'JCMT review already exist'):
            self.db.set_jcmt_review(
                role_class,
                reviewer_id,
                review=null_tuple(JCMTReview)._replace(
                    expertise=JCMTReviewerExpertise.INTERMEDIATE),
                is_update=False)

        self.db.set_jcmt_review(role_class,
                                reviewer_id,
                                review=null_tuple(JCMTReview)._replace(
                                    expertise=JCMTReviewerExpertise.EXPERT),
                                is_update=True)

        jcmt_review = self.db.get_jcmt_review(reviewer_id)

        self.assertIsNotNone(jcmt_review)
        self.assertIsInstance(jcmt_review, JCMTReview)
        self.assertEqual(jcmt_review.expertise, JCMTReviewerExpertise.EXPERT)

        # Add a second review and try a multiple-reviewer search.
        reviewer_id_2 = self.db.add_reviewer(role_class, proposal_id,
                                             person_id,
                                             role_class.CTTEE_SECONDARY)

        self.assertIsInstance(reviewer_id_2, int)

        self.db.set_jcmt_review(
            role_class,
            reviewer_id_2,
            review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.INTERMEDIATE),
            is_update=False)

        result = self.db.search_jcmt_review(
            reviewer_id=[reviewer_id, reviewer_id_2])

        self.assertIsInstance(result, ResultCollection)

        self.assertEqual(set(result.keys()), set((reviewer_id, reviewer_id_2)))
Exemplo n.º 16
0
 def make_review(id, role, rating, weight):
     return null_tuple(Reviewer)._replace(id=101,
                                          role=role,
                                          review_rating=rating,
                                          review_weight=weight,
                                          review_state=ReviewState.DONE)
Exemplo n.º 17
0
    def test_attach_review(self):
        types = self.view.get_call_types()
        roles = self.view.get_reviewer_roles()

        # Create test proposals and reviews.
        proposal_1 = self._create_test_proposal('20A', 'P', types.STANDARD)
        proposal_2 = self._create_test_proposal('20A', 'P', types.STANDARD)

        person_id = self.db.add_person('Test Reviewer')

        review_1a = self.db.add_reviewer(
            roles, proposal_1, person_id, roles.CTTEE_PRIMARY)
        self.db.set_review(
            roles, review_1a, text='test', format_=FormatType.PLAIN,
            assessment=None, rating=30, weight=None,
            note='', note_format=FormatType.PLAIN, note_public=False,
            is_update=False)
        self.db.set_jcmt_review(
            roles, review_1a, review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.EXPERT), is_update=False)

        review_1b = self.db.add_reviewer(
            roles, proposal_1, person_id, roles.CTTEE_SECONDARY)
        self.db.set_review(
            roles, review_1b, text='test', format_=FormatType.PLAIN,
            assessment=None, rating=40, weight=None,
            note='', note_format=FormatType.PLAIN, note_public=False,
            is_update=False)
        self.db.set_jcmt_review(
            roles, review_1b, review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.INTERMEDIATE), is_update=False)

        review_2a = self.db.add_reviewer(
            roles, proposal_2, person_id, roles.CTTEE_PRIMARY)
        self.db.set_review(
            roles, review_2a, text='test', format_=FormatType.PLAIN,
            assessment=None, rating=50, weight=None,
            note='', note_format=FormatType.PLAIN, note_public=False,
            is_update=False)
        self.db.set_jcmt_review(
            roles, review_2a, review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.NON_EXPERT), is_update=False)

        review_2b = self.db.add_reviewer(
            roles, proposal_2, person_id, roles.CTTEE_SECONDARY)
        self.db.set_review(
            roles, review_2b, text='test', format_=FormatType.PLAIN,
            assessment=None, rating=60, weight=None,
            note='', note_format=FormatType.PLAIN, note_public=False,
            is_update=False)
        self.db.set_jcmt_review(
            roles, review_2b, review=null_tuple(JCMTReview)._replace(
                expertise=JCMTReviewerExpertise.NON_EXPERT), is_update=False)

        # Expected data for tests.
        expect = {
            proposal_1: {
                review_1a: {'r': 30, 'e': JCMTReviewerExpertise.EXPERT},
                review_1b: {'r': 40, 'e': JCMTReviewerExpertise.INTERMEDIATE},
            },
            proposal_2: {
                review_2a: {'r': 50, 'e': JCMTReviewerExpertise.NON_EXPERT},
                review_2b: {'r': 60, 'e': JCMTReviewerExpertise.NON_EXPERT},
            }
        }

        # Original proposal collection should not have JCMT review info.
        c = self.db.search_proposal(with_reviewers=True, with_review_info=True)
        self.assertIsInstance(c, ProposalCollection)

        self.assertEqual(set(c.keys()), set(expect.keys()))

        for proposal in c.values():
            proposal_expect = expect[proposal.id]

            rc = proposal.reviewers
            self.assertIsInstance(rc, ReviewerCollection)
            self.assertEqual(set(rc.keys()), set(proposal_expect.keys()))

            for reviewer in rc.values():
                reviewer_expect = proposal_expect[reviewer.id]

                self.assertEqual(reviewer.review_rating, reviewer_expect['r'])
                self.assertIsNone(reviewer.review_extra)

        # Attach the JCMT information.
        self.view.attach_review_extra(self.db, c)

        # Check that the collection has been updated correctly.
        self.assertEqual(set(c.keys()), set(expect.keys()))

        for proposal in c.values():
            proposal_expect = expect[proposal.id]

            rc = proposal.reviewers
            self.assertIsInstance(rc, ReviewerCollection)
            self.assertEqual(set(rc.keys()), set(proposal_expect.keys()))

            for reviewer in rc.values():
                reviewer_expect = proposal_expect[reviewer.id]

                self.assertEqual(reviewer.review_rating, reviewer_expect['r'])
                self.assertIsInstance(reviewer.review_extra, JCMTReview)
                self.assertEqual(reviewer.review_extra.expertise,
                                 reviewer_expect['e'])