def test_create(self):
        """Team owners can create classrooms with themselves as contact."""
        team = Team.create(name='Team Foo',
                           captain_id='User_cap',
                           program_id=self.program.uid)
        team.put()
        user = User.create(name='User Foo',
                           email='*****@*****.**',
                           owned_teams=[team.uid])
        user.put()
        response = self.testapp.post_json(
            '/api/classrooms',
            {
                'name': 'Classroom Foo',
                'team_id': team.uid,
                'code': 'a a',
                'contact_id': user.uid
            },
            headers=self.login_headers(user),
        )
        # Make sure the response is right.
        response_dict = json.loads(response.body)
        classroom = Classroom.get_by_id(response_dict['uid'])
        self.assertEqual(
            response.body,
            json.dumps(classroom.to_client_dict(),
                       default=util.json_dumps_default),
        )
        # Make sure the contact is set.
        self.assertEqual(classroom.contact_id, user.uid)

        # Clear the user's cookie so we can use the app as other people.
        self.testapp.reset()
        return user, classroom
示例#2
0
    def test_resolve_mismatch(self):
        team, classroom, captain, contact = self.create()

        old_captain_id = captain.uid
        old_contact_id = contact.uid
        new_captain_id = 'User_newcaptain'
        new_contact_id = 'User_newcontact'

        captain = User.resolve_id_mismatch(captain, new_captain_id)
        contact = User.resolve_id_mismatch(contact, new_contact_id)

        self.assertEqual(captain.uid, new_captain_id)
        self.assertEqual(contact.uid, new_contact_id)

        fetched_captain = User.get_by_id(new_captain_id)
        fetched_contact = User.get_by_id(new_contact_id)

        self.assertIsNotNone(fetched_captain)
        self.assertIsNotNone(fetched_contact)

        fetched_team = Team.get_by_id(team.uid)
        self.assertEqual(fetched_team.captain_id, new_captain_id)

        fetched_classroom = Classroom.get_by_id(classroom.uid)
        self.assertEqual(fetched_classroom.contact_id, new_contact_id)
示例#3
0
    def get_team_reports(self, team_id):
        user = self.get_current_user()
        team = Team.get_by_id(team_id)

        if not team:
            return self.http_not_found()

        if not owns(user, team) and not has_captain_permission(user, team):
            return self.http_forbidden("Only team members can list reports.")

        # Limit access to preview reports, super admin only.
        show_preview_reports = True if user.super_admin else False
        # Returns both team- and class-level reports.
        all_reports = Report.get_for_team(team.uid, show_preview_reports)

        # Any team member can see the team reports...
        allowed_reports = [r for r in all_reports if not r.classroom_id]

        # ...but limit access to classroom reports.
        classroom_reports = [r for r in all_reports if r.classroom_id]
        classroom_ids = [r.classroom_id for r in classroom_reports]
        classrooms = {c.uid: c for c in Classroom.get_by_id(classroom_ids)}

        for report in classroom_reports:
            if has_contact_permission(user, classrooms[report.classroom_id]):
                allowed_reports.append(report)

        # Add a custom link for users to access each report.
        self.write([
            dict(r.to_client_dict(), link=self.report_link(r))
            for r in allowed_reports
        ])
示例#4
0
    def test_patch_with_disassociated_participant(self):
        """If a participant exists with no classrooms, they are found."""
        other, teammate, contact, captain, team, classroom, _ = self.create()

        student_id = 'disassociated'

        # Add an participant who is disassociated from all classrooms.
        ppt = Participant.create(
            team_id=team.uid,
            classroom_ids=[],
            student_id=student_id,
        )
        ppt.put()

        def postBody(ppt, id_modifier=''):
            ppt = ppt.copy()
            ppt['student_id'] += id_modifier
            return {'method': 'POST', 'path': '/api/participants', 'body': ppt}

        response = self.testapp.patch_json(
            '/api/participants',
            [{
                'method': 'POST',
                'path': '/api/participants',
                'body': {
                    'team_id': team.uid,
                    'classroom_id': classroom.uid,
                    'student_id': student_id,
                },
            }],
            headers=jwt_headers(contact),
        )
        response_list = json.loads(response.body)

        # The provided student id matched the disassociated user and the
        # db used that uid.
        self.assertEqual(len(response_list), 1)
        self.assertEqual(response_list[0]['uid'], ppt.uid)
        self.assertEqual(
            Participant.get_by_id(ppt.uid).classroom_ids,
            [classroom.uid],
        )

        # `num_students` has incremented after adding new participant to
        # classroom.
        updated_classroom = Classroom.get_by_id(classroom.uid)
        self.assertEqual(
            updated_classroom.num_students,
            classroom.num_students + 1,
        )
示例#5
0
    def release_and_notify(self, week):
        # Update all reports for a given week and set preview = False, _and_
        # create notifications for all related users.

        # Capture which reports are previews before we release them. We'll base
        # the notifications on this list to make sure we're not notifying about
        # any reports that aren't previews.
        reports_to_release = Report.get_previews(week)

        # This is the "release" part.
        num_reports_released = Report.release_previews(week)

        # Now we have to load a bunch of data from the related teams,
        # classrooms, and users in order to create notifications.
        if len(reports_to_release) != num_reports_released:
            logging.error(
                "Report.get_previews() ({}) and Report.release_previews() "
                "({}) didn't hit the same number of rows.".format(
                    len(reports_to_release), num_reports_released))

        team_ids = {r.team_id for r in reports_to_release}
        classroom_ids = {
            r.classroom_id
            for r in reports_to_release if r.classroom_id
        }

        # Load all related teams and classrooms as a batch.
        teams = Team.get_by_id(team_ids)
        classrooms = Classroom.get_by_id(classroom_ids)

        t_index = {t.uid: t for t in teams}
        c_index = {c.uid: c for c in classrooms}
        p_index = {p.uid: p for p in Program.get()}
        notes = []
        for r in reports_to_release:
            team = t_index.get(r.team_id)
            program = p_index.get(team.program_id)
            classroom = c_index.get(r.classroom_id, None)
            result = self.notify_for_single_report(program, team, classroom)
            # result might be a list or None
            if result:
                notes += result

        Notification.put_multi(notes)

        return num_reports_released
    def test_full_delete_by_captain(self):
        captain, contact, classroom, report1, report2 = \
            self.create_for_delete()

        url = '/api/classrooms/{}'.format(classroom.uid)
        headers = self.login_headers(captain)

        # Delete the classroom.
        self.testapp.delete(url, headers=headers, status=204)

        # Expect the classroom and related reports are gone from the db.
        self.assertIsNone(Classroom.get_by_id(classroom.uid))
        self.assertIsNone(Report.get_by_id(report1.uid))
        self.assertIsNone(Report.get_by_id(report2.uid))

        # Api should show a 404.
        self.testapp.get(url, headers=headers, status=404)
        self.testapp.delete(url, headers=headers, status=404)
示例#7
0
    def test_update_remove_from_all(self):
        other, teammate, contact, captain, team, classroom, ppnt = self.create(
        )

        # Updating to have no classrooms DOES NOT delete the participant and
        # updates classroom counts. We keep participants even if they're not on
        # any classrooms so that if they're re-added their uid (which must be
        # synced with Neptune) remains the same.
        self.testapp.put_json(
            '/api/participants/{}'.format(ppnt.uid),
            {'classroom_ids': []},
            headers=jwt_headers(captain),
        )
        fetched_ppnt = Participant.get_by_id(ppnt.uid)
        self.assertIsNotNone(fetched_ppnt)
        self.assertEqual(fetched_ppnt.classroom_ids, [])

        fetched_classroom = Classroom.get_by_id(classroom.uid)
        self.assertEqual(fetched_classroom.num_students,
                         classroom.num_students - 1)
示例#8
0
    def test_update_remove_from_multiple_rosters(self):
        """When ppt is updated to have one fewer classroom id."""
        other, teammate, contact, captain, team, classroom, _ = self.create()

        # Make a second classroom to associate with.
        classroom2 = Classroom.create(
            name="Second Classroom",
            team_id=team.uid,
            contact_id='User_contact',
            code="bar",
        )

        # Start with a ppt on two classrooms.
        ppnt = Participant.create(
            team_id=team.uid,
            classroom_ids=[classroom.uid, classroom2.uid],
            student_id='toremove',
        )
        ppnt.put()

        # Make sure count of students starts out correct.
        classroom2.num_students = 1
        classroom2.put()

        # Remove them from just one of their classrooms.
        response = self.testapp.put_json(
            '/api/participants/{}'.format(ppnt.uid),
            {'classroom_ids': [classroom.uid]},  # classroom2 removed
            headers=jwt_headers(contact),
        )

        # Check they're still on the other classroom.
        fetched = Participant.get_by_id(ppnt.uid)
        self.assertEqual(fetched.classroom_ids, [classroom.uid])

        # Check that classroom size is updated.
        fetched_classroom2 = Classroom.get_by_id(classroom2.uid)
        self.assertEqual(fetched_classroom2.num_students,
                         classroom2.num_students - 1)
示例#9
0
    def test_patch_batch(self):
        """custom PATCH handling: add a batch of participants"""
        (other, teammate, contact, captain, team, classroom,
         ppnt) = self.create()

        class1ppt1 = {
            'first_name': u'Je\u017cu',
            'last_name': u'Kl\u0105tw',
            'team_id': team.uid,
            'classroom_id': classroom.uid,
            'student_id': 'Student_one',
        }
        class1ppt2 = {
            'first_name': u'Ppt',
            'last_name': u'One-Two',
            'team_id': team.uid,
            'classroom_id': classroom.uid,
            'student_id': 'Student_two',
        }
        class2ppt1 = {
            'first_name': u'Ppt',
            'last_name': u'Two-One',
            'team_id': team.uid,
            'classroom_id': 'Classroom_other',
            'student_id': 'Student_other',
        }

        def postBody(ppt, id_modifier=''):
            ppt = ppt.copy()
            ppt['student_id'] += id_modifier
            return {'method': 'POST', 'path': '/api/participants', 'body': ppt}

        # Can't create for other classrooms.
        for user in (other, teammate):
            self.testapp.patch_json(
                '/api/participants',
                [postBody(class1ppt1)],
                headers=jwt_headers(user),
                status=403,
            )

        # Can't create for mixed classrooms.
        self.testapp.patch_json(
            '/api/participants',
            [postBody(class1ppt1), postBody(class2ppt1)],
            headers=jwt_headers(captain),
            status=400,
        )

        # Success for contacts and captains.
        for user in (contact, captain):
            response = self.testapp.patch_json(
                '/api/participants',
                [
                    postBody(class1ppt1, user.uid),
                    postBody(class1ppt2, user.uid),
                ],
                headers=jwt_headers(user),
            )
            response_list = json.loads(response.body)
            self.assertEqual(len(response_list), 2)
            fetched1 = Participant.get_by_id(response_list[0]['uid'])
            fetched2 = Participant.get_by_id(response_list[1]['uid'])
            self.assertIsNotNone(fetched1)
            self.assertIsNotNone(fetched2)

        # `num_students` has incremented after adding new participant to
        # classroom.
        updated_classroom = Classroom.get_by_id(classroom.uid)
        self.assertEqual(
            updated_classroom.num_students,
            # two created by contact, and two by captain
            classroom.num_students + 2 + 2)
示例#10
0
def owns(user, id_or_entity):
    """Does this user own the object in question?"""

    # Supers own everything.
    if user.super_admin:
        return True

    # Standardize id vs. entity, saving db trips where possible.
    # Also, javascript-like closures are surprisingly hard in python.
    # https://stackoverflow.com/questions/2009402/read-write-python-closures
    class entity_closure(object):
        def __init__(self, id_or_entity):
            if isinstance(id_or_entity, basestring):
                self.uid = str(id_or_entity)
                self._entity = None
            else:
                self.uid = id_or_entity.uid
                self._entity = id_or_entity
            self.kind = SqlModel.get_kind(self.uid)

        def __call__(self):
            # Calling code may have pulled the entity for us already, no sense
            # in doing it again; on the other hand, merely an id may be
            # sufficient, depending on the kind, so only run this code if its
            # called.
            if self._entity is None:
                self._entity = SqlModel.kind_to_class(kind).get_by_id(self.uid)
            return self._entity

    get_entity = entity_closure(id_or_entity)
    kind, uid = get_entity.kind, get_entity.uid

    if kind == 'Classroom':
        classroom = get_entity()
        team = Team.get_by_id(classroom.team_id)
        team_member = classroom.team_id in user.owned_teams
        owns = team_member or is_supervisor_of_team(user, team)
    elif kind == 'Digest':
        digest = get_entity()
        owns = user.uid == digest.user_id
    elif kind == 'Metric':
        owns = False  # only supers
    elif kind == 'Organization':
        owns = (uid in user.owned_organizations
                or uid in user.get_networked_organization_ids())
    elif kind == 'Network':
        owns = uid in user.owned_networks
    elif kind == 'Team':
        # Users can own a team directly ("team member"), or can own one of the
        # organizations associated with the team.
        team = get_entity()
        owns = uid in user.owned_teams or is_supervisor_of_team(user, team)
    elif kind == 'Survey':
        survey = get_entity()
        team = Team.get_by_id(survey.team_id)
        team_member = survey.team_id in user.owned_teams
        owns = team_member or is_supervisor_of_team(user, team)
    elif kind == 'Report':
        report = get_entity()
        if report.classroom_id:
            classroom = Classroom.get_by_id(report.classroom_id)
            owns = user.uid == classroom.contact_id
        else:
            team = Team.get_by_id(report.team_id)
            team_member = report.team_id in user.owned_teams
            owns = team_member or is_supervisor_of_team(user, team)
    elif kind == 'Cycle':
        cycle = get_entity()
        team = Team.get_by_id(cycle.team_id)
        owns = (cycle.team_id in user.owned_teams
                or is_supervisor_of_team(user, team))
    elif kind == 'Response':
        response = get_entity()
        team = Team.get_by_id(response.team_id)
        owns = (
            response.user_id == user.uid
            or (response.type == Response.TEAM_LEVEL_SYMBOL and
                (response.team_id in user.owned_teams or  # members, captain
                 is_supervisor_of_team(user, team)  # org supervisors
                 )))
    elif kind == 'Participant':
        ppnt = get_entity()
        classrooms = Classroom.get_by_id(ppnt.classroom_ids)
        team = Team.get_by_id(ppnt.team_id)
        owns = (any(has_contact_permission(user, c) for c in classrooms)
                or has_captain_permission(user, team))
    elif kind == 'User':
        owns = uid == user.uid  # no slavery!
    else:
        raise Exception("Ownership does not apply to " + uid)

    return owns
示例#11
0
    def test_delete_removes_related(self):
        user = User.create(name='foo', email='*****@*****.**')

        team = Team.create(name='Team Foo', captain_id=user.uid,
                           program_id=self.demo_program.uid)
        team.put()

        survey = Survey.create(team_id=team.uid)
        survey.put()

        user.owned_teams = [team.uid]
        user.put()

        classroom1 = Classroom.create(
            name='Classroom One',
            code='trout viper',
            team_id=team.uid,
            contact_id='User_contact',
            num_students=22,
            grade_level='9-12',
        )
        classroom1.put()
        classroom2 = Classroom.create(
            name='Classroom Two',
            code='trout viper',
            team_id=team.uid,
            contact_id='User_contact',
            num_students=22,
            grade_level='9-12',
        )
        classroom2.put()

        report1 = Report.create(
            team_id=team.uid,
            classroom_id=classroom1.uid,
            filename='report1.pdf',
            gcs_path='/upload/abc',
            size=10,
            content_type='application/pdf',
        )
        report1.put()
        report2 = Report.create(
            team_id=team.uid,
            classroom_id=classroom2.uid,
            filename='report2.pdf',
            gcs_path='/upload/def',
            size=10,
            content_type='application/pdf',
        )
        report2.put()

        url = '/api/teams/{}'.format(team.uid)
        headers = self.login_headers(user)

        # Delete the team.
        self.testapp.delete(url, headers=headers, status=204)

        # Expect the survey, classrooms, and related reports are gone from the
        # db.
        self.assertIsNone(Survey.get_by_id(survey.uid))
        self.assertIsNone(Classroom.get_by_id(classroom1.uid))
        self.assertIsNone(Classroom.get_by_id(classroom2.uid))
        self.assertIsNone(Report.get_by_id(report1.uid))
        self.assertIsNone(Report.get_by_id(report2.uid))
示例#12
0
    def post(self):
        """Save references to reports, and perhaps the report file itself.

        Has two modes: accept a file, in which case the file is saved to GCS, or
        a dataset id. In both cases a Report is inserted referencing the
        file/dataset.

        Either `team_id` or `classroom_id` must be provided. If team_id, then
        the report's classroom_id is empty (these are "team-level" reports). If
        classroom_id, the corresponding team_id is looked up (these are
        "classroom-level" reports).
        """

        # Allow RServe to call this endpoint, then fall back on regular auth.
        user, error = self.authenticate_rserve()
        if not user:
            user = self.get_current_user()
            error = ''

        # Replaces function of `requires_auth = True`.
        if user.user_type == 'public':
            return self.http_unauthorized()

        if not user.super_admin:
            return self.http_forbidden()

        org_id = self.get_param('organization_id', str, None)
        team_id = self.get_param('team_id', str, None)
        classroom_id = self.get_param('classroom_id', str, None)
        params = self.get_params({
            'dataset_id': str,
            'filename': unicode,
            'issue_date': str,
            'notes': unicode,
            'preview': bool,
            'template': str,
        })

        # Lookup related objects.
        classroom = None
        if classroom_id:
            classroom = Classroom.get_by_id(classroom_id)
            if not classroom:
                return self.http_bad_request(
                    "Classroom not found: {}".format(classroom_id))
            team_id = classroom.team_id

        team = None
        if team_id:
            # May be set for team reports, or via lookup for class reports.
            team = Team.get_by_id(team_id)
            if not team:
                return self.http_bad_request(
                    "Team not found: {}".format(team_id))

        org = None
        if org_id:
            org = Organization.get_by_id(org_id)
            if not org:
                return self.http_bad_request(
                    "Organization not found: {}".format(org_id))

        content_type = self.request.headers['Content-Type']
        is_form = 'multipart/form-data' in content_type
        is_json = 'application/json' in content_type

        if is_form:
            report = self.save_file(
                params['filename'],
                self.request.POST['file'],
                org_id,
                team_id,
                classroom_id,
            )
        elif is_json:
            kwargs = {
                'classroom_id': classroom_id,
                'dataset_id': params['dataset_id'],
                'filename': params['filename'],
                'issue_date': params.get('issue_date', None),
                'notes': params.get('notes', None),
                'organization_id': org_id,
                'team_id': team_id,
                'template': params['template'],
            }

            # Some params we may want to avoid including at all, so that if
            # they're absent they'll take the default value defined in the db.
            if params.get('preview', None) is not None:
                kwargs['preview'] = params['preview']

            report = Report.create(**kwargs)
        else:
            return self.http_bad_request(
                "Only supported content types are 1) multipart/form-data for "
                "uploading files and 2) application/json for datasets. Got {}".
                format(self.request.headers['Content-Type']))

        saved_report = Report.put_for_index(report, 'parent-file')

        self.write(saved_report)