def parse_activity_scores(self, activity_attempt): ''' Processes activity scores recieved from the mapper. This is called in the mapper callback function. Each time a student attempts a GCB question or a Quizly exercise, a tag-assessment Event is created. This processes such events to extract the number of attempts the student made and the answers. Events are time-stamped and recorded by user_id. They include the instance_id of the Component that triggered the Event. Both GCB questions and Quizly exercises have an instance_id. However, Quizly exercises don't have question_id and need special processing. Use the Dashboard to see what the data looks like for Events: https://console.cloud.google.com/datastore/entities/query? project=ram8647&ns=ns_mobileCSP&kind=EventEntity ''' if activity_attempt.source == 'tag-assessment': data = transforms.loads(activity_attempt.data) instance_id = data['instanceid'] if GLOBAL_DEBUG: logging.debug('***********RAM************** data[instanceid] = ' + instance_id) timestamp = int( (activity_attempt.recorded_on - datetime.datetime(1970, 1, 1)).total_seconds()) # Get information about the course's questions (doesn't include Quizly exercises yet) questions = self.params['questions_by_usage_id'] valid_question_ids = self.params['valid_question_ids'] assessment_weights = self.params['assessment_weights'] group_to_questions = self.params['group_to_questions'] student = Student.get_by_user_id(activity_attempt.user_id) # Get this student's answers so far student_answers = self.activity_scores.get(student.email, {}) if GLOBAL_DEBUG: logging.debug('***RAM*** student answers = ' + str(student_answers)) answers = event_transforms.unpack_check_answers( # No Quizly answers in here data, questions, valid_question_ids, assessment_weights, group_to_questions, timestamp) # Add the score to right lesson # NOTE: This was throwing an exception on Quizly exercises. Shouldn't happen now try: # If the event is tag-assessment and has no quid, it's a Quizly exercise if not 'quid' in data: self.parse_quizly_scores(data, instance_id, timestamp, student, student_answers) else: self.parse_question_scores(instance_id, questions, student_answers, answers, student, timestamp) except Exception as e: logging.error('***********RAM************** bad instance_id: %s %s\n%s', str(instance_id), e, traceback.format_exc()) if GLOBAL_DEBUG: logging.debug('***RAM*** activity_scores ' + str(self.activity_scores)) return self.activity_scores
def get_activity_scores(cls, student_user_ids, course, force_refresh=True): """Retrieve activity data for student using EventEntity. For each student, launch a Query of EventEntities to retrieve student scores. The Query is launched as a map-reduce background process that will return up to 500 results, reporting back every second. It reports back by calling the map_fn callback, which in turn calls parse_activity scores. As soon as the Query is launched (in the background) the foreground process calls build_missing_scores() to construct a student_answer.dict that will be updated as score data for that student is received. Events properties include a userid (a number) and a source (e.g., tag-assessement), a recorded-on date (timestamp) and data (a dictionary). Here's a typeical data dict: {"loc": {"city": "mililani", "language": "en-US,en;q=0.8", "locale": "en_US", "country": "US", "region": "hi", "long": -158.01528099999999, "lat": 21.451331, "page_locale": "en_US"}, "instanceid": "yOkVTqWogdaF", "quid": "5733935958982656", "score": 1, "location": "https://mobilecsp-201608.appspot.com/mobilecsp/unit?unit=1&lesson=45", "answer": [0, 1, 2, 4], "type": "McQuestion", "user_agent": "Mozilla/5.0 ..."} Note that it includes the unit_id and lesson_id as part of the Url """ # Instantiate parser object cached_date = datetime.datetime.now() activityParser = ActivityScoreParser() if force_refresh: activityParser.params = activityParser.build_additional_mapper_params( course.app_context) # Launch a background Query for each student's activity data. This is expensive. for user_id in student_user_ids: # if GLOBAL_DEBUG: logging.debug('***RAM*** launching a query for student ' + str(user_id)) mapper = models_utils.QueryMapper( EventEntity.all().filter('user_id in', [user_id]) \ .filter('recorded_on >= ', cls.CUTOFF_DATE), \ batch_size=1000, report_every=1000) # Callback function -- e.g., 45-50 callbacks per query def map_fn(activity_attempt): # if GLOBAL_DEBUG: # logging.debug('***RAM*** map_fn ' + str(activity_attempt)) activityParser.parse_activity_scores(activity_attempt) mapper.run(map_fn) # In the foreground create the student_answer_dict, which is stored at: # activity_scores[student][unit][lesson][sequence] where sequence is # the question's sequential position within the lesson. # So each question in the lesson will have a question_answer_dict. activityParser.build_missing_scores() #Lets cache results for each student for user_id in student_user_ids: cached_student_data = {} cached_student_data['date'] = cached_date student = Student.get_by_user_id(user_id) cached_student_data[ 'scores'] = activityParser.activity_scores.get( student.email, {}) cached_student_data[ 'attempts'] = activityParser.num_attempts_dict.get( student.email, {}) MemcacheManager.set( cls._memcache_key_for_student(student.email), cached_student_data) else: uncached_students = [] for student_id in student_user_ids: if student_id != '': student = Student.get_by_user_id(student_id) temp_email = student.email temp_mem = cls._memcache_key_for_student(temp_email) scores_for_student = MemcacheManager.get(temp_mem) if scores_for_student: cached_date = scores_for_student['date'] activityParser.activity_scores[ student_id] = scores_for_student['scores'] activityParser.num_attempts_dict[ student_id] = scores_for_student['scores'] else: uncached_students.append(student_id) if len(uncached_students) > 0: if cached_date == None or datetime.datetime.now( ) < cached_date: cached_date = datetime.datetime.now() activityParser.params = activityParser.build_additional_mapper_params( course.app_context) for user_id in uncached_students: mapper = models_utils.QueryMapper( EventEntity.all().filter('user_id in', [user_id]) \ .filter('recorded_on >= ', cls.CUTOFF_DATE), \ batch_size=1000, report_every=1000) def map_fn(activity_attempt): activityParser.parse_activity_scores(activity_attempt) mapper.run(map_fn) activityParser.build_missing_scores() #Lets cache results for each student for user_id in uncached_students: cached_student_data = {} cached_student_data['date'] = cached_date student = Student.get_by_user_id(user_id) cached_student_data[ 'scores'] = activityParser.activity_scores.get( student.email, {}) MemcacheManager.set( cls._memcache_key_for_student(student.email), cached_student_data) score_data = {} score_data['date'] = cached_date score_data['scores'] = activityParser.activity_scores score_data['attempts'] = activityParser.num_attempts_dict if GLOBAL_DEBUG: logging.debug('***RAM*** get_activity_scores returning scores: ' + str(score_data['scores'])) return score_data
def parse_activity_scores(self, activity_attempt): ''' Processes activity scores recieved from the mapper. This is called in the mapper callback function. Each time a student attempts a GCB question or a Quizly exercise, a tag-assessment Event is created. This processes such events to extract the number of attempts the student made and the answers. Events are time-stamped and recorded by user_id. They include the instance_id of the Component that triggered the Event. Both GCB questions and Quizly exercises have an instance_id. However, Quizly exercises don't have question_id and need special processing. Use the Dashboard to see what the data looks like for Events: https://console.cloud.google.com/datastore/entities/query? project=ram8647&ns=ns_mobileCSP&kind=EventEntity ''' if activity_attempt.source == 'tag-assessment': data = transforms.loads(activity_attempt.data) instance_id = data['instanceid'] if GLOBAL_DEBUG: logging.debug( '***********RAM************** data[instanceid] = ' + instance_id) timestamp = int((activity_attempt.recorded_on - datetime.datetime(1970, 1, 1)).total_seconds()) # Get information about the course's questions (doesn't include Quizly exercises yet) questions = self.params['questions_by_usage_id'] valid_question_ids = self.params['valid_question_ids'] assessment_weights = self.params['assessment_weights'] group_to_questions = self.params['group_to_questions'] student = Student.get_by_user_id(activity_attempt.user_id) # Get this student's answers so far student_answers = self.activity_scores.get(student.email, {}) if GLOBAL_DEBUG: logging.debug('***RAM*** student answers = ' + str(student_answers)) answers = event_transforms.unpack_check_answers( # No Quizly answers in here data, questions, valid_question_ids, assessment_weights, group_to_questions, timestamp) # Add the score to right lesson # NOTE: This was throwing an exception on Quizly exercises. Shouldn't happen now try: # If the event is tag-assessment and has no quid, it's a Quizly exercise if not 'quid' in data: self.parse_quizly_scores(data, instance_id, timestamp, student, student_answers) else: self.parse_question_scores(instance_id, questions, student_answers, answers, student, timestamp) except Exception as e: logging.error( '***********RAM************** bad instance_id: %s %s\n%s', str(instance_id), e, traceback.format_exc()) if GLOBAL_DEBUG: logging.debug('***RAM*** activity_scores ' + str(self.activity_scores)) return self.activity_scores
def get_activity_scores(cls, student_user_ids, course, force_refresh = True): """Retrieve activity data for student using EventEntity. For each student, launch a Query of EventEntities to retrieve student scores. The Query is launched as a map-reduce background process that will return up to 500 results, reporting back every second. It reports back by calling the map_fn callback, which in turn calls parse_activity scores. As soon as the Query is launched (in the background) the foreground process calls build_missing_scores() to construct a student_answer.dict that will be updated as score data for that student is received. Events properties include a userid (a number) and a source (e.g., tag-assessement), a recorded-on date (timestamp) and data (a dictionary). Here's a typeical data dict: {"loc": {"city": "mililani", "language": "en-US,en;q=0.8", "locale": "en_US", "country": "US", "region": "hi", "long": -158.01528099999999, "lat": 21.451331, "page_locale": "en_US"}, "instanceid": "yOkVTqWogdaF", "quid": "5733935958982656", "score": 1, "location": "https://mobilecsp-201608.appspot.com/mobilecsp/unit?unit=1&lesson=45", "answer": [0, 1, 2, 4], "type": "McQuestion", "user_agent": "Mozilla/5.0 ..."} Note that it includes the unit_id and lesson_id as part of the Url """ # Instantiate parser object cached_date = datetime.datetime.now() activityParser = ActivityScoreParser() if force_refresh: activityParser.params = activityParser.build_additional_mapper_params(course.app_context) # Launch a background Query for each student's activity data. This is expensive. for user_id in student_user_ids: # if GLOBAL_DEBUG: # logging.debug('***RAM*** launching a query for student ' + str(user_id)) mapper = models_utils.QueryMapper( EventEntity.all().filter('user_id in', [user_id]) \ .filter('recorded_on >= ', cls.CUTOFF_DATE), \ batch_size=1000, report_every=1000) # Callback function -- e.g., 45-50 callbacks per query def map_fn(activity_attempt): # if GLOBAL_DEBUG: # logging.debug('***RAM*** map_fn ' + str(activity_attempt)) activityParser.parse_activity_scores(activity_attempt) mapper.run(map_fn) # In the foreground create the student_answer_dict, which is stored at: # activity_scores[student][unit][lesson][sequence] where sequence is # the question's sequential position within the lesson. # So each question in the lesson will have a question_answer_dict. activityParser.build_missing_scores() #Lets cache results for each student for user_id in student_user_ids: cached_student_data = {} cached_student_data['date'] = cached_date student = Student.get_by_user_id(user_id) cached_student_data['scores'] = activityParser.activity_scores.get(student.email, {}) cached_student_data['attempts'] = activityParser.num_attempts_dict.get(student.email, {}) MemcacheManager.set(cls._memcache_key_for_student(student.email),cached_student_data) else: uncached_students = [] for student_id in student_user_ids: if student_id != '': student = Student.get_by_user_id(student_id) temp_email = student.email temp_mem = cls._memcache_key_for_student(temp_email) scores_for_student = MemcacheManager.get(temp_mem) if scores_for_student: cached_date = scores_for_student['date'] activityParser.activity_scores[student_id] = scores_for_student['scores'] activityParser.num_attempts_dict[student_id] = scores_for_student['scores'] else: uncached_students.append(student_id) if len(uncached_students) > 0: if cached_date == None or datetime.datetime.now() < cached_date: cached_date = datetime.datetime.now() activityParser.params = activityParser.build_additional_mapper_params(course.app_context) for user_id in uncached_students: mapper = models_utils.QueryMapper( EventEntity.all().filter('user_id in', [user_id]) \ .filter('recorded_on >= ', cls.CUTOFF_DATE), \ batch_size=1000, report_every=1000) def map_fn(activity_attempt): activityParser.parse_activity_scores(activity_attempt) mapper.run(map_fn) activityParser.build_missing_scores() #Lets cache results for each student for user_id in uncached_students: cached_student_data = {} cached_student_data['date'] = cached_date student = Student.get_by_user_id(user_id) cached_student_data['scores'] = activityParser.activity_scores.get(student.email, {}) MemcacheManager.set(cls._memcache_key_for_student(student.email),cached_student_data) score_data = {} score_data['date'] = cached_date score_data['scores'] = activityParser.activity_scores score_data['attempts'] = activityParser.num_attempts_dict if GLOBAL_DEBUG: logging.debug('***RAM*** get_activity_scores returning scores: ' + str(score_data['scores'])) return score_data