def do(self): if self.access_restricted(['student', 'public']): return program_info = {} programs = self.api.see('program', {'see': 'abbreviation'}) for p in programs: s_config = Program.get_app_configuration(p.abbreviation, 'student') t_config = Program.get_app_configuration(p.abbreviation, 'teacher') program_config = Program.get_app_configuration(p.abbreviation) program_info[p.id] = { 'student_outline': s_config['outline'] if s_config else None, 'teacher_outline': t_config['outline'] if t_config else None, 'normal_school_instructions': markdown.markdown( getattr(program_config, 'normal_school_instructions', '')), 'alias_school_instructions': markdown.markdown( getattr(program_config, 'alias_school_instructions', '')), } return 'dashboard.html', { 'program_info': program_info, 'cohort_aggregation_data_template': Cohort.aggregation_data_template, }
def program_outline(self, program_id): program = self.get('program', {'id': program_id})[0] t_conf = Program.get_app_configuration(program.abbreviation, 'teacher') s_conf = Program.get_app_configuration(program.abbreviation, 'student') return { 'teacher': t_conf['outline'] if t_conf else None, 'student': s_conf['outline'] if s_conf else None }
def test_unique_retrieval(self): """Test that QualtricsLink.get_link catches inconsistent results proving that get_link() handles inconsistency and doesn't return deleted (already used and thus non-unique) links""" p = Program.create(name='Test Program', abbreviation='TP1') # Create two links and put them to the db a = QualtricsLink.create(key_name='test_a', link='test_a', session_ordinal=1, program=p.id) b = QualtricsLink.create(key_name='test_b', link='test_b', session_ordinal=1, program=p.id) a.put() b.put() # db.get by key forces consistency a, b = db.get([a.key(), b.key()]) # delete link a to put db into inconsistent state. db.delete(a) # get_link pulls in alphabetical order by link, so # a is retrieved first, then b. b_link = QualtricsLink.get_link(p, 1, default_link='qualtrics.com') # If we have consistency issues, get_link() may not see that a has been # deleted, and thus return it rather than b. We want to assert that we # don't have those issues. self.assertEquals(b_link, 'test_b')
def check_params(self, params): """Raises exceptions if there are problems with request data.""" # Make sure all parameters are present. required = ['program', 'user', 'activity_ordinal'] missing = set(required) - set(params.keys()) if missing: raise Exception("Missing required parameters: {}.".format(missing)) program_list = self.internal_api.get( 'program', {'abbreviation': params['program']}) if len(program_list) is not 1: raise Exception("Invalid program: {}.".format(params['program'])) else: program = program_list[0] # User must exist and be associated with provided program. user = self.internal_api.get_from_path('user', params['user']) if user.assc_program_list[0] != program.id: raise Exception("Invalid program for user: {} {}.".format( params['program'], params['user'])) # Activity ordinal must be among the available program sessions. o = int(params['activity_ordinal']) student_program_outline = Program.get_app_configuration( program.abbreviation, 'student')['outline'] modules_match = [ module.get('activity_ordinal', None) is o for module in student_program_outline ] if not any(modules_match): raise Exception("Invalid activity_ordinal: {}.".format(o)) return program
def do(self, program_abbreviation): # No access is restricted. abbr = program_abbreviation.upper() program_list = self.internal_api.get('program', {'abbreviation': abbr}) if len(program_list) is not 1: logging.error("Bad public registration link: {}.".format( self.request.path)) return self.http_not_found() program = program_list[0] program_config = Program.get_app_configuration(program.abbreviation) # Not all programs are public access. Check before continuing. if not getattr(program_config, 'public_registration', False): return self.http_not_found() # Registration requires the user to select a school. Provide them a # list of existing schools to pick from. cohorts = self.internal_api.get('cohort', {'assc_program_list': program.id}) school_ids = [c.assc_school_list[0] for c in cohorts] schools = self.internal_api.get_by_ids(school_ids) template_path = '/programs/{}/public_registration.html'.format(abbr) params = { 'program': program.to_dict(), 'program_config': program_config, 'school_list': [s.to_dict() for s in schools], } template_directory = '' # i.e. from the root of the app return template_path, params, template_directory
def do(self): new_programs = [] deleted_programs = [] current_programs = [] current_abbreviations = [] user = self.get_current_user() if user.user_type != 'god': raise core.PermissionDenied() # Check what program directories exist path = os.path.join(os.getcwd(), 'programs') new_abbreviations = util.get_immediate_subdirectories(path) # Check what program entities exist current_programs = Program.all().filter('deleted =', False).fetch(11) if len(current_programs) > 10: raise Exception("Too many programs. Limit is 10.") # Check them against each other. for p in current_programs: if p.abbreviation in new_abbreviations: current_abbreviations.append(p.abbreviation) new_abbreviations.remove(p.abbreviation) else: p.deleted = True deleted_programs.append(p) db.put(deleted_programs) # Anything remaining in the list is new. for a in new_abbreviations: program_config = Program.get_app_configuration(a) p = Program.create(abbreviation=a, name=program_config.name) current_programs.append(p) new_programs.append(p) db.put(new_programs) return { 'success': True, 'data': { 'deleted_programs': [p.to_dict() for p in deleted_programs], 'new_programs': [p.to_dict() for p in new_programs], 'current_programs': [p.to_dict() for p in current_programs], } }
def do(self): if self.access_restricted( ['researcher', 'school_admin', 'teacher', 'student', 'public']): return # We want to avoid populating the same datastore twice. So attempt to # detect if the populate script has already been done. programs_exist = Program.all().count() > 0 # Load the available short links. short_links = [] for sl in ShortLink.all().filter('deleted =', False).run(): short_links.append(sl.to_dict()) return 'god.html', { 'programs_exist': programs_exist, 'short_links': short_links, }
def do(self, program_id, user_type, activity_ordinal): # URL args come in as srings, we need this to be an int. activity_ordinal = int(activity_ordinal) # We need to find the default link for this program. # First look up the program. program = self.internal_api.get_from_path('program', program_id) # Then the module for the relevant session. module = Program.get_activity_configuration(program.abbreviation, user_type, activity_ordinal) default_link = module.get('qualtrics_default_link', None) if default_link is None: raise Exception('No default link set in config.') link = QualtricsLink.get_link(program, int(activity_ordinal), default_link) return {'success': True, 'data': link}
def get_reminders_by_date(self, date_string=None): emails_by_user = collections.defaultdict(list) # render each unsent reminder for program in Program.list_all(): for user_type in ['student', 'teacher']: for reminder in program.get_reminders(user_type): new_emails = self._get_emails_from_reminder( date_string, program, user_type, reminder) for user_id, emails in new_emails.items(): emails_by_user[user_id] += emails # consolidate # If a user has more than one message then they should be merged into a # single message. merged = {} for user_id, emails in emails_by_user.items(): merged[user_id] = Reminder.merge_multiple_emails(emails) return merged.values()
def do(self, program_abbreviation, template_name): abbr = program_abbreviation.upper() program_list = self.internal_api.get('program', {'abbreviation': abbr}) if len(program_list) is not 1: logging.error("Bad program link: {}.".format(self.request.path)) return self.http_not_found() program = program_list[0] program_config = Program.get_app_configuration(abbr) if getattr(program_config, 'public_registration', False): # Allow public users to look up cohorts, because this program is # public-access. temp_api = self.internal_api elif self.access_restricted(['public']): # Programs which use public registration don't require sign-in to # view documents. If this is not a public registration program and # the user is not signed in, redirect them to login. return else: temp_api = self.api # Some documents require the cohort id to display the cohort code. cohort_id = self.request.get('cohort') # Validate the id. It should exist and match the program. try: cohort = temp_api.get_from_path('cohort', cohort_id) except: cohort = None template_path = 'programs/{}/{}.html'.format(program_abbreviation, template_name) params = { 'cohort': cohort, 'template_name': template_name, 'program': program, } template_directory = '' # i.e. from the root of the app return template_path, params, template_directory
def test_default(self): """If no link entities exist, the default link is returned.""" p = Program.create(name='Test Program', abbreviation='TP1') default_link = 'qualtrics.com' link = QualtricsLink.get_link(p, 1, default_link=default_link) self.assertEquals(link, default_link)
def do(self, program_abbreviation, template_name): if self.access_restricted(['public']): return abbr = program_abbreviation.upper() program_list = self.internal_api.get('program', {'abbreviation': abbr}) if len(program_list) is not 1: logging.error("Bad program link: {}.".format(self.request.path)) return self.http_not_found() program = program_list[0] # We'll display a warning to the user if this is not true. valid_url = True # The cohort is also a critical part of the app, otherwise we can't # record data. cohort = None cohort_id = self.request.get('cohort') # Validate the id. It should exist and match the program. if cohort_id == '': valid_url = False else: cohort = self.api.get_from_path('cohort', cohort_id) if cohort is None: valid_url = False else: if program.id not in cohort.assc_program_list: valid_url = False # The classroom is critical IF the user is a student. classroom = None classroom_id = self.request.get('classroom') if template_name == 'student' and cohort is not None: # Validate. if classroom_id == '': valid_url = False else: classroom = self.api.get_from_path('classroom', classroom_id) if classroom is None: valid_url = False else: if cohort.id not in classroom.assc_cohort_list: valid_url = False if not valid_url: return self.http_not_found() # get program app config app_config = Program.get_app_configuration(abbr, template_name) template_path = 'programs/{}/{}.html'.format(program_abbreviation, template_name) params = { 'show_main_nav': False, 'show_footer': False, 'template_name': template_name, 'program': program, 'cohort': cohort, 'classroom': classroom, 'config_json': json.dumps(app_config), 'server_date_time': str(datetime.datetime.now()), } template_directory = '' # i.e. from the root of the app return template_path, params, template_directory
def do(self, program_abbreviation): user = self.get_current_user() # initialize variables registration_requested = self.request.get('registration_requested') redirect = str(self.request.get('redirect')) login_url = '/yellowstone_login' success_message = None error_message = None program_id = None program_name = None show_registration = False show_google_login = True # if a program was specified, look it up and adjust future redirects if program_abbreviation: program_abbreviation = program_abbreviation.upper() login_url += '/' + program_abbreviation # User has arrived via a program-specific link. Look up the program # to get its id (for association) and name (for a welcome message). results = self.internal_api.see( 'program', {'abbreviation': program_abbreviation}) if len(results) is 1: # If this is a public-registration program, then deny access. program_conf = Program.get_app_configuration( program_abbreviation) if getattr(program_conf, 'public_registration', False): return self.http_not_found() # Otherwise we can continue... program = results[0] program_id = program.id program_name = program.name show_registration = True elif len(results) is 0: # There's no program with this abbreviation. return self.http_not_found() # Set up how the user will COME BACK from google login, which will be # used if they click the google login button. google_redirect = '{}?registration_requested=google'.format(login_url) if redirect: google_redirect += '&redirect={}'.format(redirect) if program_id: google_redirect += '&program={}'.format(program_id) google_login_url = app_engine_users.create_login_url(google_redirect) if registration_requested: # They're signed in with google or facebok, allow registration IF # they've arrive with a program id. If they're a new user AND # there's no program id, display a failure message. auth_response = self.authenticate(auth_type=registration_requested) if auth_response is None: error_message = "Error during sign in. Please try again." redirect = None elif auth_response is False and program_id is None: error_message = ("Unable to register.<br>Please use the " "registration link provided by your school.") redirect = None else: # OK to register and/or sign in; register() does both user, user_is_new = self.register(program_id, registration_requested) if user is False: error_message = ("You already have a PERTS account with " "that email address.") redirect = None else: if (user.user_type in ['teacher', 'school_admin'] and not user.registration_complete): # Special case: force certain users to take additional # registration steps. The registration_email_sent flag # displays a confirmation that a registration email was # sent. redirect = '/registration_survey?' if user_is_new: redirect += 'registration_email_sent=true&' if program_id: redirect += 'program={}&'.format(program_id) # Any users who have successfully registered or logged in via the code # above and are still here should be sent on (this is normal for google # users, who are returning from a google login page). # Also, any signed in user with a home page arriving at login should be # redirected to their home page. if (user is not False and user.user_type != 'public' and user.user_type in config.user_home_page): if not redirect: redirect = config.user_home_page[user.user_type] # else leave redirect set per the URL self.redirect(redirect) return # If user has just finished a password reset, show them a message. if self.request.get('password_reset_successful') == 'true': success_message = "Your password has been reset." show_google_login = False return 'login.html', { 'show_registration': show_registration, 'program_id': program_id, 'program_name': program_name, 'google_login_url': google_login_url, 'error_message': error_message, 'success_message': success_message, 'show_google_login': show_google_login, }
def register(self, program_id, auth_type, username=None, password=None, cohort_id=None): """Logs in users and registers them if they're new. Raises exceptions when the system appears to be broken (e.g. redundant users). Returns tuple( user - mixed, False when given non-sensical data by users (e.g. registering an existing email under a new auth type) or matching user entity, user_is_new - bool, True if user was newly registered ) """ user_is_new = False if auth_type not in config.allowed_auth_types: raise Exception("Bad auth_type: {}.".format(auth_type)) if auth_type == 'direct': if None in [username, password]: raise Exception("Credentials incomplete.") creation_kwargs = { 'login_email': username, 'auth_id': 'direct_' + username, 'plaintext_password': password, # it WILL be hashed later } # These are the third party identity providers we currently know # how to handle. See util_handlers.BaseHandler.get_third_party_auth(). elif auth_type in ['google']: # may raise CredentialsMissing creation_kwargs = self.get_third_party_auth(auth_type) # Check that the user hasn't already registered in two ways. # 1) If the email matches but the auth type is different, we reject # the request to register so the UI can warn the user. email_match_params = {'login_email': creation_kwargs['login_email']} email_matches = self.internal_api.get('user', email_match_params) for user in email_matches: if user.auth_type() != auth_type: user = False return (user, user_is_new) # 2) If the auth_id matches, they tried to register when they should # have logged in. Just log them in. auth_match_params = {'auth_id': creation_kwargs['auth_id']} auth_matches = self.internal_api.get('user', auth_match_params) if len(auth_matches) == 1: # they already have an account user = auth_matches[0] # This user hasn't already registered. Register them. elif len(auth_matches) == 0: user_is_new = True # Having collected the user's information, build a user object. creation_kwargs['user_type'] = 'teacher' # Include the program id as an initial relationship (see # config.optional_associations). creation_kwargs['program'] = program_id user = self.api.create('user', creation_kwargs) # Refresh the api with the user's new privileges. self.api = Api(user) # Send them an email to confirm that they have registered. # The email text is hard coded in the program app. program = self.api.get_from_path('program', program_id) program_conf = Program.get_app_configuration(program.abbreviation) mandrill.send( to_address=creation_kwargs['login_email'], subject=program_conf.registration_email_subject, body=mandrill.render_markdown( program_conf.registration_email_body), template_data={'email': creation_kwargs['login_email'], 'cohort_id': cohort_id} ) logging.info('url_handlers.BaseHandler.register()') logging.info('sending an email to: {}' .format(creation_kwargs['login_email'])) # There's a big problem. else: logging.error("Two matching users! {}".format(auth_match_params)) # Sort the users by modified time, so we take the most recently # modified one. auth_matches = sorted( auth_matches, key=lambda u: u.modified, reverse=True) user = auth_matches[0] # Sign in the user. self.session['user'] = user.id return (user, user_is_new)
def do(self): params = util.get_request_dictionary(self.request) # Check that the requested program allows public registration. program = self.internal_api.get_from_path('program', params['program']) program_config = Program.get_app_configuration(program.abbreviation) if not getattr(program_config, 'public_registration', False): user = self.get_current_user() logging.error("User {} attempted public registration on program " "{}, but it isn't allowed.".format( user, program.abbreviation)) # Create a school admin based on the user's information. # They get the special auth_type 'public', preventing them from # appearing in sign-in queries or reset-password queries. # However, the user entity will still hold data and be associated with # the created schools. params['user']['user_type'] = 'school_admin' params['user']['auth_id'] = 'public_' + params['user']['login_email'] school_admin = self.internal_api.create('user', params['user']) # If the school already exists, use the id to find the right cohort. if 'existing_school_id' in params: s_id = params['existing_school_id'] cohort_list = self.internal_api.get('cohort', {'assc_school_list': s_id}) if len(cohort_list) is not 1: raise Exception("Problem with public registration: found {} " "cohorts for school {}".format( len(cohort_list), s_id)) cohort = cohort_list[0] school = None classroom = None activities = None # Otherwise, create a school, cohort, and classroom based on the # provided data. else: school = self.internal_api.create('school', params['new_school']) cohort = self.internal_api.create( 'cohort', { 'name': params['new_school']['name'], 'school': school.id, 'program': program.id, 'promised_students': params['promised_students'], }) classroom = self.internal_api.create( 'classroom', { 'name': 'All Students', 'program': program.id, 'cohort': cohort.id, 'user': school_admin.id, }) activities = self.internal_api.init_activities( 'student', school_admin.id, program.id, cohort_id=cohort.id, classroom_id=classroom.id) # Whether the cohort is new or exisiting, make the new user owner of it self.internal_api.associate('set_owner', school_admin, cohort) # Send an email to the user with all the information they need to # participate. mandrill.send(to_address=school_admin.login_email, subject=program_config.registration_email_subject, body=mandrill.render_markdown( program_config.registration_email_body), template_data={ 'email': school_admin.login_email, 'cohort_id': cohort.id }) logging.info('api_handlers.CreatePublicSchoolHandler') logging.info('sending an email to: {}'.format( school_admin.login_email)) return { 'success': True, 'data': { 'user': school_admin.to_dict(), 'program': program.to_dict(), 'school': school.to_dict() if school else None, 'cohort': cohort.to_dict(), 'classroom': classroom.to_dict() if classroom else None, 'activities': ([a.to_dict() for a in activities] if activities else None), } }