Ejemplo n.º 1
0
    def test_delete_removes_team(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()

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

        survey = Survey.create(team_id=team.uid)
        survey.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 team is gone from the db.
        self.assertIsNone(Team.get_by_id(team.uid))

        # Api should show a 404.
        self.testapp.get(url, headers=headers, status=404)
        self.testapp.delete(url, headers=headers, status=404)
Ejemplo n.º 2
0
    def test_delete_disassociates_teams(self):
        """When you delete an org, associated teams lose their org id."""
        user, org_dict = self.test_create()
        teams = [
            Team.create(
                name="Team Foo",
                organization_ids=[org_dict['uid']],
                captain_id='User_captain',
                program_id=self.program.uid,
            ),
            Team.create(
                name="Team Bar",
                organization_ids=[org_dict['uid']],
                captain_id='User_captain',
                program_id=self.program.uid,
            ),
        ]
        Team.put_multi(teams)

        response = self.testapp.delete('/api/organizations/{}'.format(
            org_dict['uid']),
                                       headers=self.login_headers(user),
                                       status=204)

        # Make sure the teams have lost their association to the org.
        for t in teams:
            fetched = Team.get_by_id(t.uid)
            self.assertNotIn(org_dict['uid'], fetched.organization_ids)
Ejemplo n.º 3
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)
Ejemplo n.º 4
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
        ])
Ejemplo n.º 5
0
    def post(self, team_id, date_str=None):
        if date_str:
            today = datetime.strptime(date_str, config.iso_date_format).date()
        else:
            today = date.today()

        # Guaranteed to have start and end dates.
        cycle = Cycle.get_current_for_team(team_id, today)
        if not cycle:
            logging.info(
                "Either the team doesn't exist, or they don't have a cycle "
                "matching the current date. Doing nothing.")
            return

        team = Team.get_by_id(team_id)
        classrooms = Classroom.get(team_id=team_id)

        if len(classrooms) == 0:
            logging.info("No classrooms, setting participation to 0.")
            cycle.students_completed = 0
        else:
            ppn = get_participation(cycle, classrooms)
            num_complete = 0
            for code, counts in ppn.items():
                complete_count = next(
                    (c for c in counts if c['value'] == '100'), None)
                num_complete += complete_count['n'] if complete_count else 0
            cycle.students_completed = num_complete

        cycle.put()
Ejemplo n.º 6
0
    def test_task_data_too_large(self):
        user = User.create(name='foo', email='*****@*****.**')
        user.put()

        team_params = {'name': 'Team Foo', 'program_id': self.demo_program.uid}

        # A property is too long.
        self.testapp.post_json(
            '/api/teams',
            dict(team_params, task_data={'foo': 'x' * 10**5}),
            headers=self.login_headers(user),
            status=413,
        )

        # Too many properties.
        self.testapp.post_json(
            '/api/teams',
            dict(
                team_params,
                task_data={'foo{}'.format(x): 'x' for x in range(10**3)},
            ),
            headers=self.login_headers(user),
            status=413,
        )

        # Both posts should have prevented teams from being stored.
        self.assertEqual(len(Team.get()), 0)

        # Successful POST
        response = self.testapp.post_json(
            '/api/teams',
            dict(team_params, task_data={'safe': 'data'}),
            headers=self.login_headers(user),
        )
        response_dict = json.loads(response.body)
        put_url = '/api/teams/{}'.format(response_dict['uid'])

        # Same errors but for PUT
        # A property is too long.
        self.testapp.put_json(
            put_url,
            {'task_data': {'foo': 'x' * 10**5}},
            headers=self.login_headers(user),
            status=413,
        )
        # Too many properties.
        self.testapp.put_json(
            put_url,
            {'task_data': {'foo{}'.format(x): 'x' for x in range(10**3)}},
            headers=self.login_headers(user),
            status=413,
        )

        # Puts should have left body unchanged.
        self.assertEqual(
            Team.get_by_id(response_dict['uid']).task_data,
            {'safe': 'data'},
        )
Ejemplo n.º 7
0
 def test_delete_by_captain(self):
     """Captains can delete their teams."""
     user, team_dict = self.create()
     response = self.testapp.delete(
         '/api/teams/{}'.format(team_dict['uid']),
         headers=self.login_headers(user),
     )
     self.assertIsNone(Team.get_by_id(team_dict['uid']))
     return response, user, team_dict
Ejemplo n.º 8
0
 def test_delete_other_forbidden(self):
     user, team_dict = self.create()
     other = User.create(name='other', email='*****@*****.**')
     other.put()
     response = self.testapp.delete(
         '/api/teams/{}'.format(team_dict['uid']),
         headers=self.login_headers(other),
         status=403
     )
     self.assertIsNotNone(Team.get_by_id(team_dict['uid']))
Ejemplo n.º 9
0
 def test_delete_own_forbidden(self):
     team = Team.create(name='foo', captain_id='User_cap',
                        program_id=self.demo_program.uid)
     team.put()
     user = User.create(name='foo', email='*****@*****.**',
                        owned_teams=[team.uid])
     user.put()
     response = self.testapp.delete(
         '/api/teams/{}'.format(team.uid),
         headers=self.login_headers(user),
         status=403
     )
     self.assertIsNotNone(Team.get_by_id(team.uid))
Ejemplo n.º 10
0
    def post(self, team_id, date_str=None):
        survey = Survey.get(team_id=team_id)[0]
        if date_str:
            today = datetime.strptime(date_str, config.iso_date_format).date()
        else:
            today = date.today()

        # Cycle ultimately comes from Cycle.get_current_for_team() and so is
        # guaranteed to have start and end dates.
        cycle = survey.should_notify(today)
        if not cycle:
            # This task is run every week, but only actually send notifications
            # if the date matches the survey interval.
            return

        team = Team.get_by_id(survey.team_id)
        program = Program.get_by_id(team.program_id)
        classrooms = Classroom.get(team_id=team_id)
        users = User.query_by_team(team_id)

        if len(classrooms) == 0:
            pct_complete_by_id = {}
        else:
            ppn = get_participation(cycle, classrooms)
            pct_complete_by_id = self.participation_to_pct(ppn, classrooms)

        # Get all the responses once to save trips to the db. Redact them later
        # according to the relevate user.
        unsafe_responses = Response.get_for_teams_unsafe([team_id],
                                                         parent_id=cycle.uid)

        to_put = []
        for user in users:
            if user.receive_email:
                safe_responses = Response.redact_private_responses(
                    unsafe_responses, user)
                email = cycle_emailers.create_cycle_email(
                    program.label,
                    user,  # recipient
                    users,
                    team,
                    classrooms,
                    safe_responses,
                    cycle,
                    pct_complete_by_id,
                )

                to_put.append(email)

        ndb.put_multi(to_put)
Ejemplo n.º 11
0
 def test_change_captain_forbidden(self):
     team = Team.create(name='foo', captain_id='User_cap',
                        program_id=self.demo_program.uid)
     team.put()
     user = User.create(name='foo', email='*****@*****.**',
                        owned_teams=[team.uid])
     user.put()
     response = self.testapp.put_json(
         '/api/teams/{}'.format(team.uid),
         {'captain_id': user.uid},
         headers=self.login_headers(user),
         status=403,
     )
     team = Team.get_by_id(team.uid)
     self.assertNotEqual(team.captain_id, user.uid)
Ejemplo n.º 12
0
    def get(self, parent_type, rel_id):
        user = self.get_current_user()
        team = Team.get_by_id(rel_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 get responses.")

        parent_id = self.get_param('parent_id', str, None)

        # We return empty dictionaries for the `body` property of some
        # responses (private responses belonging to other users).
        responses = Response.get_for_teams(user, [team.uid], parent_id)

        self.write(responses)
Ejemplo n.º 13
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
Ejemplo n.º 14
0
def team_membership_removal_allowed(subject, requestor, params):
    """Is the requestor allowed to remove the subject from a team?"""
    if 'owned_teams' not in params:
        return False

    removed_ids = set(subject.owned_teams).difference(params['owned_teams'])
    if len(removed_ids) == 0:
        # We're not removing teams, no need to provide permission.
        return False
    if len(removed_ids) > 1:
        logging.info("Team membership can only be revoked one team at a time.")
        return False

    team = Team.get_by_id(removed_ids.pop())  # may be None

    return (
        # Case 1: Requestor is team captain of team being removed.
        (team and has_captain_permission(requestor, team)) or
        # Case 2: Remove oneself from a team.
        subject.uid == requestor.uid or
        # Case 3: Requestor is super_admin
        requestor.super_admin)
Ejemplo n.º 15
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)
Ejemplo n.º 16
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
Ejemplo n.º 17
0
    def get(self, user_id=None, team_id=None):
        complete = False

        # Determine authenticated user based on JWT token
        # @todo: can we apply jti or some other rule to make sure this URL isn't
        # inappropriately shareable?
        token = self.request.get('token', None)
        payload, error = jwt_helper.decode(token)
        if not payload or error:
            return self.http_forbidden()

        auth_user = User.get_by_id(payload['user_id'])

        user = User.get_by_id(user_id)
        team = Team.get_by_id(team_id)

        if not user or not team:
            return self.http_not_found()

        # The authenticated user can only retrieve their own certificate.
        # The authenticated user must own the team that they are requesting the
        #   certificate for.
        if not auth_user == user and not owns(auth_user, team):
            return self.http_forbidden()

        classrooms = Classroom.get(
            contact_id=user.uid,
            team_id=team_id,
        )

        cycles = Cycle.get(
            team_id=team_id,
            order='ordinal',
        )

        if len(classrooms) > 0 and len(cycles) > 0:
            cycle_participation = self.get_cycle_participation_pct(
                cycles,
                classrooms,
            )

            participation_complete = self.has_completed_three_cycles(
                cycle_participation)
        else:
            cycle_participation = [{
                'ordinal': c.ordinal,
                'pct': 0,
            } for c in cycles]
            participation_complete = False

        exit_survey_complete = self.has_completed_exit_survey(
            user,
            team_id,
        )

        if (exit_survey_complete and participation_complete):
            complete = True

        if (complete):
            # If a teacher has successfully completed participation for three
            # cycles, the certificate should not show any incomplete cycles
            # because they aren't relevant for the requirement of receiving the
            # completion certificate. See #1223.
            cycles_to_display = [
                c for c in cycle_participation if c['pct'] >= 80
            ][0:3]
        else:
            cycles_to_display = cycle_participation

        if util.is_localhost():
            neptune_protocol = 'http'
            neptune_domain = 'localhost:8080'
        else:
            neptune_protocol = 'https'
            neptune_domain = os.environ['NEPTUNE_DOMAIN']

        self.write(
            'completion.html',
            neptune_protocol=neptune_protocol,
            neptune_domain=neptune_domain,
            complete=complete,
            user_to_display=user,
            team=team,
            cycles_to_display=cycles_to_display,
            exit_survey_complete=exit_survey_complete,
        )