def test_delete_code_allowed(self): code = 'trout viper' path = '/api/codes/{}'.format(code.replace(' ', '-')) pc = ProjectCohort.create( code=code, organization_id='triton', program_label='triton', ) pc.put() pc.key.get() # simulate consistency, code fetches are eventual token = jwt_helper.encode({ 'user_id': 'User_foo', 'email': '*****@*****.**', 'allowed_endpoints': ['DELETE //neptune{}'.format(path)], }) self.testapp.delete( path, headers={'Authorization': 'Bearer ' + token}, status=204, ) # Project cohort AND Unique should be gone self.assertIsNone(pc.key.get()) unique_key = ndb.Key('Unique', ProjectCohort.uniqueness_key(code)) self.assertIsNone(unique_key.get())
def test_queue_org_welcome_back(self): Program.mock_program_config('p1', {'project_tasklist_template': []}) # Case 1: New PC, returning. returning_pc1 = ProjectCohort.create( program_label='p1', organization_id='Organization_returning', project_id='Project_returning', cohort_label='2020', ) returning_pc1.put() returning_pc2 = ProjectCohort.create( program_label='p1', organization_id='Organization_returning', project_id='Project_returning', cohort_label='2019', created=datetime.datetime.now() - datetime.timedelta(days=365)) returning_pc2.put() # Case 2: New PC, but not returning. new_pc = ProjectCohort.create( program_label='p1', organization_id='Organization_new', project_id='Project_new', ) new_pc.put() # Case 3: Old PC (not created in the day). old_pc = ProjectCohort.create( program_label='p1', organization_id='Organization_old', project_id='Project_old', created=datetime.datetime.now() - datetime.timedelta(hours=48), ) old_pc.put() # Some tasks are created on put. We're not interested in these. creation_tasks = self.taskqueue_stub.get_filtered_tasks() templates = [ self.create_mandrill_template('p1-{}'.format( auto_prompt.ORG_WELCOME_BACK_SUFFIX)), ] auto_prompt.queue_org_welcome_back(templates) tasks = self.taskqueue_stub.get_filtered_tasks() num_new_tasks = len(tasks) - len(creation_tasks) # Only the returning pc should have a task queued. self.assertEqual(num_new_tasks, 1) expected_url = '/task/email_project/Project_returning/p1-org-welcome-back' self.assertIn(expected_url, [t.url for t in tasks]) Program.reset_mocks()
def test_batch_participation(self): user = User.create(email='*****@*****.**') user.put() pc_kwargs = { 'program_label': self.program_label, 'cohort_label': self.cohort_label, } pcs = [ ProjectCohort.create(**pc_kwargs), ProjectCohort.create(**pc_kwargs), ] ndb.put_multi(pcs) all_pds = [] for pc in pcs: pds = mock_one_finished_one_unfinished( 1, 'Participant_unfinished', 'Participant_finished', pc_id=pc.uid, code=pc.code, program_label=self.program_label, cohort_label=self.cohort_label, ) all_pds += pds # Forbidden without allowed endpoints. pc_ids = [pc.uid for pc in pcs] self.testapp.get( '/api/project_cohorts/participation?uid={}&uid={}'.format(*pc_ids), headers=jwt_headers(user), status=403) # Running various queries works as expected. self.batch_participation(user, pcs) # Simulate a new pd being written to the first pc by clearing that # memcache key. The server should fall back to sql and still give the # same results. id_key = ParticipantData.participation_by_pc_cache_key(pcs[0].uid) code_key = ParticipantData.participation_by_pc_cache_key(pcs[0].code) self.assertIsNotNone(memcache.get(id_key)) self.assertIsNotNone(memcache.get(code_key)) memcache.delete(id_key) memcache.delete(code_key) self.batch_participation(user, pcs) # Now with everything cached, clearing the db and running the same # queries again should have the same result. ParticipantData.delete_multi(all_pds) self.batch_participation(user, pcs)
def test_fix_open_responses(self): """Non-triton pcs shouldn't have the open response param.""" non_triton_empty = ProjectCohort.create(program_label='demo-program', ) non_triton_only = ProjectCohort.create( program_label='demo-program', survey_params_json=json.dumps({ 'show_open_response_questions': 'true', })) non_triton_extra = ProjectCohort.create( program_label='demo-program', survey_params_json=json.dumps({ 'foo': 'bar', 'show_open_response_questions': 'true', }), ) triton = ProjectCohort.create( program_label='triton', survey_params_json=json.dumps({ 'learning_conditions': "feedback-for-growth", 'show_open_response_questions': 'true', }), ) def get_mapped(pc): operation = next(fix_open_responses(pc), None) if operation: return operation.entity # Empty value unchanged mapped_non_triton_empty = get_mapped(non_triton_empty) self.assertEqual(mapped_non_triton_empty.survey_params, {}) # Key removed. mapped_non_triton_only = get_mapped(non_triton_only) self.assertNotIn('show_open_response_questions', mapped_non_triton_only.survey_params) # Key removed, other data preserved. mapped_non_triton_extra = get_mapped(non_triton_extra) self.assertNotIn('show_open_response_questions', mapped_non_triton_extra.survey_params) self.assertIn('foo', mapped_non_triton_extra.survey_params) # Triton pc untouched. mapped_triton = get_mapped(triton) self.assertIsNone(mapped_triton)
def test_completion_csv_allowed(self): org_id = 'Organization_foo' pc = ProjectCohort.create( organization_id=org_id, program_label=self.program_label, cohort_label=self.cohort_label, ) pc.put() user = User.create( email='*****@*****.**', owned_organizations=[org_id], ) user.put() t = AuthToken.create(user.uid) t.put() self.testapp.get( '/api/project_cohorts/{}/completion/ids.csv'.format(pc.uid), params={'token': t.token}, headers=login_headers(user.uid), # Authenticated, one-time token valid, has permission: 200. status=200, )
def create_pd_context(self): program_label = 'demo-program' program_config = Program.get_config(program_label) template = program_config['surveys'][0]['survey_tasklist_template'] project_cohort = ProjectCohort.create( program_label=program_label, organization_id='Organization_foo', project_id='Project_foo', cohort_label='2018', ) project_cohort.put() survey = Survey.create( template, program_label=program_label, organization_id='Organization_foo', project_cohort_id=project_cohort.uid, ordinal=1, ) survey.put() participant = Participant.create(name='Pascal', organization_id='PERTS') participant.put() return (project_cohort, survey, participant)
def test_put_clears_dashboard_queries(self): pc = ProjectCohort.create( program_label='demo-program', organization_id='Organization_Foo', project_id='Project_Foo', cohort_label='2018', ) org_key = util.cached_query_key( 'SuperDashboard', organization_id=pc.organization_id, ) program_cohort_key = util.cached_query_key( 'SuperDashboard', program_label=pc.program_label, cohort_label=pc.cohort_label, ) memcache.set(org_key, {'foo': 1}) memcache.set(program_cohort_key, {'foo': 1}) # This should clear memcache. pc.put() self.assertIsNone(memcache.get(org_key)) self.assertIsNone(memcache.get(program_cohort_key))
def create_project_cohort(self, cohort_date=datetime.datetime.today()): program_label = 'demo-program' cohort_label = 'demo-cohort' program = Program.get_config(program_label) org_id = 'Org_Foo' liaison_id = 'User_liaison' project = Project.create(organization_id=org_id, program_label=program_label) project.put() one_day = datetime.timedelta(days=1) cohort_config = { 'label': cohort_label, 'name': 'Demo Cohort', 'open_date': str(cohort_date - one_day), # yesterday 'close_date': str(cohort_date + one_day), # tomorrow } program['cohorts'][cohort_label] = cohort_config Program.mock_program_config( program_label, {'cohorts': { cohort_label: cohort_config }}, ) pc = ProjectCohort.create( project_id=project.uid, organization_id=org_id, program_label=program_label, cohort_label=cohort_label, liaison_id=liaison_id, ) pc.put() return pc
def delete_for_program(entity): """Permanently delete anything in the datastore from a program. Works with Projects, ProjectCohorts, and Surveys. Also requires a param set in mapreduce.yaml: `program_label`. """ params = context.get().mapreduce_spec.mapper.params if getattr(entity, 'program_label', None) != params['program_label']: return # If this is a project cohort, delete the Unique entity that serves as an # index of participation codes. key_name = ProjectCohort.uniqueness_key(getattr(entity, 'code', '')) unique_entity = ndb.Key('Unique', key_name).get() if unique_entity: yield op.db.Delete(unique_entity) # Some entities have tasks in their entity group. There's no convenient # way to query tasks directly, so delete them while we're handling their # parent. # Bypass DatastoreModel to make sure we get soft-deleted entities. for task in Task.query(ancestor=entity.key).iter(): yield op.db.Delete(task) yield op.db.Delete(entity)
def queue_checklist_nudge(templates): # Is it exactly one month before the program opens for student # participation for this cohort year? Find all orgs registered and send # them reminder email (complete checklist, participation code, etc) tasks = [] month_from_now = util.datelike_to_iso_string(datetime.date.today() + datetime.timedelta(days=30)) to_prompt = programs_with_template(templates, CHECKLIST_NUDGE_SUFFIX) for program in to_prompt: for cohort in program['cohorts'].values(): project_ids = set() if cohort['open_date'] == month_from_now: # This whole cohort needs a nudge. Get all the project cohorts. pcs = ProjectCohort.get( program_label=program['label'], cohort_label=cohort['label'], projection=['project_id'], n=float('inf'), ) for pc in pcs: url = '/task/email_project/{}/{}'.format( pc.project_id, get_slug(program['label'], CHECKLIST_NUDGE_SUFFIX)) task = taskqueue.add(url=url) tasks.append(task) return tasks
def test_override_portal_message(self): label = 'override-program' config = {'override_portal_message': 'Override message.'} Program.mock_program_config(label, config) pc = ProjectCohort.create(program_label=label) self.assertEqual( pc.to_client_dict()['portal_message'], config['override_portal_message'], )
def test_cohort(self): user = User.create(email='*****@*****.**', user_type='super_admin') user.put() pc = ProjectCohort.create( project_id='Project_001', organization_id='Organization_001', program_label=self.program_label, cohort_label=self.cohort_label, liaison_id='User_001', ) pc.put() query = ''' query PCWithCohort($uid: String) { project_cohort(uid: $uid) { program_cohort { close_date label name open_date program_description program_label program_name registration_close_date registration_open_date } } } ''' expected = { 'project_cohort': { 'program_cohort': OrderedDict( (k, self.cohort[k]) for k in sorted(self.cohort.keys())), }, } response = self.testapp.post_json( '/api/graphql', { 'query': query, 'variables': { 'uid': pc.uid }, }, headers=login_headers(user.uid), ) self.assertEqual( response.body, json.dumps(expected), )
def test_get_project_cohort_participation(self): """Get stats for all surveys in the project cohort.""" pds1 = self.mock_one_finished_one_unfinished(1) pds2 = self.mock_one_finished_one_unfinished(2) project_cohort_id = pds1[0].project_cohort_id ProjectCohort( id=project_cohort_id, program_label=self.program_label, ).put() start = datetime.date.today() end = start + datetime.timedelta(days=1) expected = [ { 'value': '1', 'n': 1, 'survey_ordinal': 1 }, { 'value': '100', 'n': 1, 'survey_ordinal': 1 }, { 'value': '1', 'n': 1, 'survey_ordinal': 2 }, { 'value': '100', 'n': 1, 'survey_ordinal': 2 }, ] result = ParticipantData.participation( project_cohort_id=project_cohort_id, start=start, end=end) self.assertEqual(result, expected) # The same result should also now be available in memcache, so if we # clear the db the result should be the same. ParticipantData.delete_multi(pds1 + pds2) result = ParticipantData.participation( project_cohort_id=project_cohort_id, start=start, end=end) self.assertEqual(result, expected) # It should also work if some other kwargs are set with value None. result = ParticipantData.participation( project_cohort_id=project_cohort_id, start=start, end=end, cohort_label=None) self.assertEqual(result, expected)
def test_create_code_creates_unique(self): """Should be an entry in Unique to ensure unique code.""" user = User.create(email='*****@*****.**') user.put() response = self.testapp.post_json( '/api/codes', {'organization_id': 'triton', 'program_label': 'triton'}, headers=self.login_headers(user) ) pc = ProjectCohort.query().fetch(1) unique = Unique.query().fetch(1) self.assertIsNotNone(pc) self.assertIsNotNone(unique)
def queue_org_welcome_back(templates): """After joining a program for the first time, welcome them.""" # Orgs signing up for a program generate a new ProjectCohort every year. # Look for recently created ones, then exclude orgs that only have one PC # in this program. yesterday = datetime.datetime.now() - datetime.timedelta(hours=24) # Which programs have a template? to_prompt = programs_with_template(templates, ORG_WELCOME_BACK_SUFFIX) tasks = [] for program in to_prompt: # Can't use .get() from DatastoreModel because of the >= query query = ProjectCohort.query( ProjectCohort.created >= yesterday, ProjectCohort.deleted == False, ProjectCohort.program_label == program['label'], ) # Gather the project ids of all applicable project cohorts. By tracking # unique project ids, we only email each org in this program once. project_ids = set() for pc in query.iter(projection=[ProjectCohort.project_id]): if pc.project_id not in project_ids: if ProjectCohort.count(project_id=pc.project_id) > 1: project_ids.add(pc.project_id) # Now send an email for each org that qualifies. for project_id in project_ids: # Then this org has done this program before, they're returning # so welcome them back. url = '/task/email_project/{}/{}'.format( project_id, get_slug(program['label'], ORG_WELCOME_BACK_SUFFIX)) task = taskqueue.add(url=url) tasks.append(task) return tasks
def get(self, template, context_id): # Attempt to treat id as as project cohort (it might be a program, or # invalid). project_cohort = ProjectCohort.get_by_id(context_id) def todt(s): return datetime.datetime.strptime(s, config.iso_date_format) if project_cohort: # This is a "real" set of instructions with data filled in. organization = Organization.get_by_id( project_cohort.organization_id) liaison = User.get_by_id(organization.liaison_id) program = Program.get_config(project_cohort.program_label) cohort = program['cohorts'][project_cohort.cohort_label] participation_open_date = todt(cohort['open_date']) # See notes in program config for why we take a day off for display. participation_close_date = (todt(cohort['close_date']) - datetime.timedelta(1)) else: # This is a generic example version of the document. try: program = Program.get_config(context_id) except ImportError: return self.http_not_found() organization = None liaison = None project_cohort = None cohort = None participation_open_date = None participation_close_date = None if template == 'custom_portal_technical_guide': # This template doesn't vary by program. template_filename = '{}.html'.format(template) else: template_filename = '{}_{}.html'.format(program['label'], template) self.write( template_filename, organization=organization, liaison=liaison, program_name=program['name'], cohort_name=cohort['name'] if cohort else '', program_description=program['description'], project_cohort=project_cohort, participation_open_date=participation_open_date, participation_close_date=participation_close_date, )
def create_with_pc(self): project = Project.create( program_label='demo-program', organization_id='Organization_Foo', ) project.put() pc = ProjectCohort.create( program_label=project.program_label, organization_id=project.organization_id, project_id=project.uid, cohort_label='2018', ) pc.put() return project.key.get(), pc.key.get()
def test_completion_anonymous_forbidden(self): pc = ProjectCohort.create( program_label=self.program_label, cohort_label=self.cohort_label, ) pc.put() user = User.create(email='*****@*****.**') user.put() self.testapp.get( '/api/project_cohorts/{}/completion'.format(pc.uid), headers=login_headers(user.uid), # user doesn't own ProjectCohort_foo, forbidden status=403, )
def create_with_project_cohort(self): checkpoint_template = { 'name': "Survey Foo", 'label': 'demo_survey__foo', 'body': "foo", 'tasks': [], } config = { 'surveys': [ { 'name': "Student Module", 'survey_tasklist_template': [checkpoint_template], }, ], } Program.mock_program_config(self.program_label, config) org = Organization.create(name='Foo Org') org.put() project = Project.create( program_label=self.program_label, organization_id=org.uid, ) project.put() pc = ProjectCohort.create( program_label=self.program_label, organization_id=org.uid, project_id=project.uid, ) pc.put() checkpoint = Checkpoint.create(parent_id='Survey_foo', ordinal=1, program_label=self.program_label, organization_id=org.uid, project_id=project.uid, project_cohort_id=pc.uid, status='incomplete', **checkpoint_template) checkpoint.put() pc.update_cached_properties() return org, project, pc, checkpoint
def test_join_cohort(self, cohort_date=datetime.date.today()): """Allowed for org admin owner of project.""" # Existing things to relate to. program_label = 'demo-program' cohort_label = 'demo-cohort' program = Program.get_config(program_label) org_id = 'Org_Foo' user = User.create(email="*****@*****.**", owned_organizations=[org_id]) project = Project.create(organization_id=org_id, program_label=program_label) user.put() project.put() # Guarantee the dates will work by mocking the cohort config. one_day = datetime.timedelta(days=1) cohort_config = { 'label': cohort_label, 'name': 'Demo Cohort', 'open_date': str(cohort_date - one_day), # yesterday 'close_date': str(cohort_date + one_day), # tomorrow } program['cohorts'][cohort_label] = cohort_config Program.mock_program_config( program_label, {'cohorts': {cohort_label: cohort_config}}, ) # Create the project cohort through the api. Any response other than # 200 will fail the test. response = self.testapp.post_json( '/api/project_cohorts', { 'project_id': project.uid, 'organization_id': org_id, 'program_label': program_label, 'cohort_label': cohort_label, 'liaison_id': user.uid, }, headers=login_headers(user.uid) ) response_dict = json.loads(response.body) return (ProjectCohort.get_by_id(response_dict['uid']), user)
def test_patch_update_codes(self): codes = ('trout viper', 'solid snake') pcs = [] for c in codes: pcs.append(ProjectCohort.create( code=c, organization_id='triton', program_label='triton', )) ndb.put_multi(pcs) for pc in pcs: pc.key.get() # simulate consistency, code fetches are eventual token = jwt_helper.encode({ 'user_id': 'User_foo', 'email': '*****@*****.**', 'allowed_endpoints': ['PUT //neptune{}'.format(path(c)) for c in codes], }) body = {'portal_message': 'hi'} response = self.testapp.patch_json( '/api/codes', [{'method': 'PUT', 'path': path(c), 'body': body} for c in codes], headers={'Authorization': 'Bearer ' + token}, ) task_names = [t['task_name'] for t in json.loads(response.body)] # PATCHing two codes should result in two tasks. for name in task_names: tasks = self.taskqueue.get_filtered_tasks(name=name) self.assertEqual(len(tasks), 1) t = tasks[0] # Running the tasks should update the codes. self.assertEqual(t.method, 'PUT') self.testapp.put_json( t.url, json.loads(t.payload), headers=t.headers, ) for pc in pcs: fetched = pc.key.get() self.assertEqual(fetched.portal_message, 'hi')
def create_demo_data(self): # Existing things to relate to. program_label = 'demo-program' cohort_label = 'demo-cohort' org_id = 'Org_Foo' user_id = 'User_Liaison' pc = ProjectCohort.create( project_id='Project_foo', organization_id=org_id, program_label=program_label, cohort_label=cohort_label, liaison_id=user_id, portal_type='custom', custom_portal_url='http://www.example.com', ) pc.put() return pc
def create_org_with_pc(self): org = Organization.create(name="Foo College") org.put() project = Project.create( program_label='demo-program', organization_id=org.uid, ) project.put() pc = ProjectCohort.create( program_label=project.program_label, organization_id=org.uid, project_id=project.uid, cohort_label='2018', ) pc.put() # Simulate consistency return org, project.key.get(), pc.key.get()
def test_dashboard_unlisted(self): """Some programs support other apps and aren't meant to be queried.""" # See #1015 Program.mock_program_config('ep19', {'listed': False}) user = User.create(email='*****@*****.**', user_type='super_admin') user.put() pc = ProjectCohort.create( program_label='ep19', organization_id='Org_foo', ) pc.put() response = self.testapp.get('/api/dashboard?program_label=ep19', headers=login_headers(user.uid)) self.assertEqual(response.body, '[]')
def test_update_project_cohort(self): org, project, pc, checkpoint = self.create_with_project_cohort() # Now changing the status should update the cached properties of the # project cohort. checkpoint.status = 'waiting' checkpoint.put() fetched = ProjectCohort.get_by_id(pc.uid) # There should be one survey checkpoint that was changed fetched_checkpoint = [ c for c in fetched.get_cached_properties()['checkpoints'] if c.parent_kind == 'Survey' ][0] self.assertEqual( fetched_checkpoint.status, 'waiting', )
def test_put_clears_dashboard_queries(self): org_id = 'Organization_Foo' program_label = 'demo-program' pc = ProjectCohort.create( program_label='demo-program', organization_id=org_id, project_id='Project_Foo', cohort_label='2018', ) pc.put() program_config = Program.get_config(program_label) template = program_config['surveys'][0]['survey_tasklist_template'] survey = Survey.create( template, program_label=program_label, organization_id=org_id, project_cohort_id=pc.uid, ordinal=1, ) survey.put() org_key = util.cached_query_key( 'SuperDashboard', organization_id=pc.organization_id, ) program_cohort_key = util.cached_query_key( 'SuperDashboard', program_label=pc.program_label, cohort_label=pc.cohort_label, ) memcache.set(org_key, {'foo': 1}) memcache.set(program_cohort_key, {'foo': 1}) # Re-fetch the org so it doesn't have an associated tasklist, which # saves checkpoints. This should clear memcache without relying on those # checkpoints. survey = survey.key.get() survey.status = 'ready' survey.put() self.assertIsNone(memcache.get(org_key)) self.assertIsNone(memcache.get(program_cohort_key))
def test_update_code_allowed(self): code = 'trout viper' path = '/api/codes/{}'.format(code.replace(' ', '-')) pc = ProjectCohort.create( code=code, organization_id='triton', program_label='triton', ) pc.put() pc.key.get() # simulate consistency, code fetches are eventual token = jwt_helper.encode({ 'user_id': 'User_foo', 'email': '*****@*****.**', 'allowed_endpoints': ['PUT //neptune{}'.format(path)], }) self.testapp.put_json( path, {'portal_message': 'hi'}, headers={'Authorization': 'Bearer ' + token} )
def test_completion_csv_forbidden(self): pc = ProjectCohort.create( program_label=self.program_label, cohort_label=self.cohort_label, ) pc.put() user = User.create(email='*****@*****.**') user.put() t = AuthToken.create(user.uid) t.put() self.testapp.get( '/api/project_cohorts/{}/completion/ids.csv'.format(pc.uid), params={'token': t.token}, headers=login_headers(user.uid), # user doesn't own ProjectCohort_foo, forbidden status=403, )
def downloaded_identifiers(user, project_cohort_id): supers = User.get(user_type='super_admin') notes = [] project_cohort = ProjectCohort.get_by_id(project_cohort_id) organization = Organization.get_by_id(project_cohort.organization_id) program = Program.get_config(project_cohort.program_label) cohort_name = program['cohorts'][project_cohort.cohort_label]['name'] for sup in supers: note = Notification.create( parent=sup, context_id=project_cohort_id, subject="IDs Downloaded", body=u"{} ({}) downloaded IDs for {}: {} {}.".format( user.name, user.email, organization.name, program['name'], cohort_name), link='/dashboard/{}'.format(project_cohort.short_uid), autodismiss=True, ) notes.append(note) ndb.put_multi(notes)
def add_completed_report_task_ids_to_project_cohort(task): """See Issue #1020 & PR #1062. In order to facilitate creating the Returning Organizations report, we need to know which Project Cohorts are returning. Returning is defined as an Organization that has participated in a previous cohort and has enrolled again. We are counting Project Cohorts as having participated if they have a completed report uploaded. This job adds the UID of report tasks that have a file attached to the associated Project Cohort's completed_report_task_ids property.""" report_task_labels = [ 'cg17_survey__report_1', 'cb17_survey__report_1', 'hg17_survey__report_2', 'sse_survey__report_1' ] if task.label in report_task_labels and task.attachment: parent_key = task.key.parent() survey = parent_key.get() project_cohort = ProjectCohort.get_by_id(survey.project_cohort_id) if (project_cohort and task.uid not in project_cohort.completed_report_task_ids): project_cohort.completed_report_task_ids.append(task.uid) yield op.db.Put(project_cohort)