Exemple #1
0
    def can_cross_empty_bridge(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        return gandalf(bridge_name)
Exemple #2
0
    def can_cross_empty_bridge(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        return gandalf(bridge_name)
Exemple #3
0
    def get(self):
        if not gandalf.gandalf("new_faster_search"):
            self.get_old()
            return

        query = self.request.get('page_search_query')
        template_values = {'page_search_query': query}
        query = query.strip()
        if len(query) < search.SEARCH_PHRASE_MIN_LENGTH:
            if len(query) > 0:
                template_values.update({
                    'query_too_short': search.SEARCH_PHRASE_MIN_LENGTH
                })
            self.render_jinja2_template("searchresults_new.html", template_values)
            return
        searched_phrases = []

        url = "http://search-rpc.khanacademy.org/solr/select/?q=%s&start=0&rows=1000&indent=on&wt=json&fl=*%%20score" % urllib.quote(query)
        try:
            logging.info("Fetching: %s" % url)
            # Allow responses to be cached for an hour
            response = urlfetch.fetch(url = url, deadline=25, headers = {'Cache-Control' : 'max-age=3600'})
            response_object = json.loads(response.content)
        except Exception, e:
            logging.error("Failed to fetch search results from search-rpc! Error: %s" % str(e))
            template_values.update({
                'server_timout': True
            })
            self.render_jinja2_template("searchresults_new.html", template_values)
            return
 def get(self):
     self.render_jinja2_template(
         'about/about_the_site.html', {
             "selected_id": "the-site",
             "approx_vid_count": Video.approx_count(),
             "gandalf_production_test": gandalf("production_test"),
         })
Exemple #5
0
    def add_global_template_values(self, template_values):
        template_values['App'] = App
        template_values['None'] = None

        if not template_values.has_key('user_data'):
            user_data = UserData.current()
            template_values['user_data'] = user_data

        user_data = template_values['user_data']

        template_values['username'] = user_data.nickname if user_data else ""
        template_values[
            'viewer_profile_root'] = user_data.profile_root if user_data else "/profile/nouser"
        template_values['points'] = user_data.points if user_data else 0
        template_values[
            'logged_in'] = not user_data.is_phantom if user_data else False
        template_values['http_host'] = os.environ["HTTP_HOST"]

        # Always insert a post-login request before our continue url
        template_values['continue'] = util.create_post_login_url(
            template_values.get('continue') or self.request.uri)
        template_values['login_url'] = (
            '%s&direct=1' % util.create_login_url(template_values['continue']))
        template_values['logout_url'] = util.create_logout_url(
            self.request.uri)

        template_values['is_mobile'] = False
        template_values['is_mobile_capable'] = False
        template_values['is_ipad'] = False

        if self.is_mobile_capable():
            template_values['is_mobile_capable'] = True
            template_values['is_ipad'] = self.is_ipad()

            if 'is_mobile_allowed' in template_values and template_values[
                    'is_mobile_allowed']:
                template_values['is_mobile'] = self.is_mobile()

        # overridable hide_analytics querystring that defaults to true in dev
        # mode but false for prod.
        hide_analytics = self.request_bool("hide_analytics", App.is_dev_server)
        template_values['hide_analytics'] = hide_analytics

        # client-side error logging
        template_values['include_errorception'] = gandalf('errorception')

        if user_data:
            goals = GoalList.get_current_goals(user_data)
            goals_data = [g.get_visible_data() for g in goals]
            if goals_data:
                template_values['global_goals'] = jsonify(goals_data)

        app_host = self.request.host.split(":")[0]
        if app_host[0].isdigit():
            app_host = app_host.partition(".")[2]
        template_values['webengage_id'] = App.webengage_id.get(
            app_host, app_host) if App.webengage_id else ""

        return template_values
Exemple #6
0
    def can_cross_all_users_blacklist(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        GandalfFilter(bridge=bridge, filter_type=filter_type, whitelist=False).put()

        return gandalf(bridge_name)
Exemple #7
0
    def can_cross_all_users_blacklist(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        GandalfFilter(bridge=bridge, filter_type=filter_type, whitelist=False).put()

        return gandalf(bridge_name)
Exemple #8
0
    def add_global_template_values(self, template_values):
        template_values["App"] = App
        template_values["None"] = None

        if not template_values.has_key("user_data"):
            user_data = UserData.current()
            template_values["user_data"] = user_data

        user_data = template_values["user_data"]
        email = user_data.email if user_data else ""
        template_values["username"] = user_data.nickname if user_data else ""
        template_values["user_email"] = email if not is_facebook_user_id(email) else ""
        template_values["viewer_profile_root"] = user_data.profile_root if user_data else "/profile/nouser"
        template_values["points"] = user_data.points if user_data else 0
        template_values["logged_in"] = not user_data.is_phantom if user_data else False
        template_values["http_host"] = os.environ["HTTP_HOST"]

        # Always insert a post-login request before our continue url
        template_values["continue"] = util.create_post_login_url(template_values.get("continue") or self.request.uri)
        template_values["login_url"] = "%s&direct=1" % util.create_login_url(template_values["continue"])
        template_values["logout_url"] = util.create_logout_url(self.request.uri)

        template_values["is_mobile"] = False
        template_values["is_mobile_capable"] = False
        template_values["is_ipad"] = False

        if self.is_mobile_capable():
            template_values["is_mobile_capable"] = True
            template_values["is_ipad"] = self.is_ipad()

            if "is_mobile_allowed" in template_values and template_values["is_mobile_allowed"]:
                template_values["is_mobile"] = self.is_mobile()

        # overridable hide_analytics querystring that defaults to true in dev
        # mode but false for prod.
        hide_analytics = self.request_bool("hide_analytics", App.is_dev_server)
        template_values["hide_analytics"] = hide_analytics

        # client-side error logging
        template_values["include_errorception"] = gandalf("errorception")

        if user_data:
            goals = GoalList.get_current_goals(user_data)
            goals_data = [g.get_visible_data() for g in goals]
            if goals_data:
                template_values["global_goals"] = jsonify(goals_data)

        app_host = self.request.host.split(":")[0]
        if app_host[0].isdigit():
            app_host = app_host.partition(".")[2]
        template_values["webengage_id"] = App.webengage_id.get(app_host, app_host) if App.webengage_id else ""

        return template_values
Exemple #9
0
    def can_cross_all_users_outside_percentage(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        filter = GandalfFilter(bridge=bridge, filter_type=filter_type, whitelist=True)
        filter.put()

        identity_percentage = BridgeFilter._identity_percentage(filter.key())

        filter.percentage = identity_percentage
        filter.put()

        return gandalf(bridge_name)
Exemple #10
0
    def can_cross_all_users_outside_percentage(self):
        bridge_name = "balrog"
        filter_type = "all-users"

        bridge = GandalfBridge.get_or_insert(bridge_name)

        filter = GandalfFilter(bridge=bridge, filter_type=filter_type, whitelist=True)
        filter.put()

        identity_percentage = BridgeFilter._identity_percentage(filter.key())

        filter.percentage = identity_percentage
        filter.put()

        return gandalf(bridge_name)
Exemple #11
0
    def get(self, exid=None):

        # TODO(david): Is there some webapp2 magic that will allow me not to
        #     repeat this URL string in main.py?
        review_mode = self.request.path == "/review" and (
            ab_test('Review Mode UI',
                conversion_name=ViewExercise._review_conversion_names,
                conversion_type=ViewExercise._review_conversion_types))

        if not exid and not review_mode:
            self.redirect("/exercise/%s" % self.request_string("exid", default="addition_1"))
            return

        user_data = models.UserData.current() or models.UserData.pre_phantom()
        user_exercise_graph = models.UserExerciseGraph.get(user_data)

        if review_mode:
            # Take the first review exercise if available
            exid = (user_exercise_graph.review_exercise_names() or
                    user_exercise_graph.proficient_exercise_names() or
                    ["addition_1"])[0]
            reviews_left_count = user_exercise_graph.reviews_left_count()

        exercise = models.Exercise.get_by_name(exid)

        if not exercise:
            raise MissingExerciseException("Missing exercise w/ exid '%s'" % exid)

        user_exercise = user_data.get_or_insert_exercise(exercise)

        # Cache these so we don't have to worry about future lookups
        user_exercise.exercise_model = exercise
        user_exercise._user_data = user_data
        user_exercise._user_exercise_graph = user_exercise_graph
        user_exercise.summative = exercise.summative

        # Temporarily work around in-app memory caching bug
        exercise.user_exercise = None

        problem_number = self.request_int('problem_number', default=(user_exercise.total_done + 1))

        user_data_student = self.request_student_user_data(legacy=True) or user_data
        if user_data_student.key_email != user_data.key_email and not user_data_student.is_visible_to(user_data):
            user_data_student = user_data

        viewing_other = user_data_student.key_email != user_data.key_email

        # Can't view your own problems ahead of schedule
        if not viewing_other and problem_number > user_exercise.total_done + 1:
            problem_number = user_exercise.total_done + 1

        # When viewing another student's problem or a problem out-of-order, show read-only view
        read_only = viewing_other or problem_number != (user_exercise.total_done + 1)

        exercise_template_html = exercise_template()

        exercise_body_html, exercise_inline_script, exercise_inline_style, data_require, sha1 = exercise_contents(exercise)
        user_exercise.exercise_model.sha1 = sha1

        user_exercise.exercise_model.related_videos = map(lambda exercise_video: exercise_video.video, user_exercise.exercise_model.related_videos_fetch())
        for video in user_exercise.exercise_model.related_videos:
            video.id = video.key().id()

        renderable = True

        if read_only:
            # Override current problem number and user being inspected
            # so proper exercise content will be generated
            user_exercise.total_done = problem_number - 1
            user_exercise.user = user_data_student.user
            user_exercise.read_only = True

            if not self.request_bool("renderable", True):
                # We cannot render old problems that were created in the v1 exercise framework.
                renderable = False

            query = models.ProblemLog.all()
            query.filter("user = "******"exercise = ", exid)

            # adding this ordering to ensure that query is served by an existing index.
            # could be ok if we remove this
            query.order('time_done')
            problem_logs = query.fetch(500)

            problem_log = None
            for p in problem_logs:
                if p.problem_number == problem_number:
                    problem_log = p
                    break

            user_activity = []
            previous_time = 0

            if not problem_log or not hasattr(problem_log, "hint_after_attempt_list"):
                renderable = False
            else:
                # Don't include incomplete information
                problem_log.hint_after_attempt_list = filter(lambda x: x != -1, problem_log.hint_after_attempt_list)

                while len(problem_log.hint_after_attempt_list) and problem_log.hint_after_attempt_list[0] == 0:
                    user_activity.append([
                        "hint-activity",
                        "0",
                        max(0, problem_log.hint_time_taken_list[0] - previous_time)
                        ])

                    previous_time = problem_log.hint_time_taken_list[0]
                    problem_log.hint_after_attempt_list.pop(0)
                    problem_log.hint_time_taken_list.pop(0)

                # For each attempt, add it to the list and then add any hints
                # that came after it
                for i in range(0, len(problem_log.attempts)):
                    user_activity.append([
                        "correct-activity" if problem_log.correct else "incorrect-activity",
                        unicode(problem_log.attempts[i] if problem_log.attempts[i] else 0),
                        max(0, problem_log.time_taken_attempts[i] - previous_time)
                        ])

                    previous_time = 0

                    # Here i is 0-indexed but problems are numbered starting at 1
                    while (len(problem_log.hint_after_attempt_list) and
                            problem_log.hint_after_attempt_list[0] == i + 1):
                        user_activity.append([
                            "hint-activity",
                            "0",
                            max(0, problem_log.hint_time_taken_list[0] - previous_time)
                            ])

                        previous_time = problem_log.hint_time_taken_list[0]
                        # easiest to just pop these instead of maintaining
                        # another index into this list
                        problem_log.hint_after_attempt_list.pop(0)
                        problem_log.hint_time_taken_list.pop(0)

                user_exercise.user_activity = user_activity

                if problem_log.count_hints is not None:
                    user_exercise.count_hints = problem_log.count_hints

                user_exercise.current = problem_log.sha1 == sha1

        is_webos = self.is_webos()
        browser_disabled = is_webos or self.is_older_ie()
        renderable = renderable and not browser_disabled

        url_pattern = "/exercise/%s?student_email=%s&problem_number=%d"
        user_exercise.previous_problem_url = url_pattern % \
            (exid, user_data_student.key_email, problem_number - 1)
        user_exercise.next_problem_url = url_pattern % \
            (exid, user_data_student.key_email, problem_number + 1)

        user_exercise_json = jsonify.jsonify(user_exercise)

        template_values = {
            'exercise': exercise,
            'user_exercise_json': user_exercise_json,
            'exercise_body_html': exercise_body_html,
            'exercise_template_html': exercise_template_html,
            'exercise_inline_script': exercise_inline_script,
            'exercise_inline_style': exercise_inline_style,
            'data_require': data_require,
            'read_only': read_only,
            'selected_nav_link': 'practice',
            'browser_disabled': browser_disabled,
            'is_webos': is_webos,
            'renderable': renderable,
            'issue_labels': ('Component-Code,Exercise-%s,Problem-%s' % (exid, problem_number)),
            'alternate_hints_treatment': ab_test('Hints or Show Solution Dec 10',
                ViewExercise._hints_ab_test_alternatives,
                ViewExercise._hints_conversion_names,
                ViewExercise._hints_conversion_types,
                'Hints or Show Solution Nov 5'),
            'reviews_left_count': reviews_left_count if review_mode else "null",
            'include_errorception': gandalf("errorception"),
            }

        self.render_jinja2_template("exercise_template.html", template_values)
Exemple #12
0
 def get(self):
     self.render_jinja2_template('about/about_the_site.html', {
         "selected_id": "the-site",
         "approx_vid_count": Video.approx_count(),
         "gandalf_production_test": gandalf("production_test"),
     })
    def add_global_template_values(self, template_values):
        template_values['App'] = App
        template_values['None'] = None

        if not template_values.has_key('user_data'):
            user_data = user_models.UserData.current()
            template_values['user_data'] = user_data

        user_data = template_values['user_data']

        template_values['username'] = user_data.nickname if user_data else ""
        template_values['viewer_profile_root'] = user_data.profile_root if user_data else "/profile/nouser/"
        template_values['points'] = user_data.points if user_data else 0
        template_values['logged_in'] = not user_data.is_phantom if user_data else False
        template_values['http_host'] = os.environ["HTTP_HOST"]

        # Always insert a post-login request before our continue url
        template_values['continue'] = util.create_post_login_url(template_values.get('continue') or self.request.uri)
        template_values['login_url'] = ('%s&direct=1' % util.create_login_url(template_values['continue']))
        template_values['logout_url'] = util.create_logout_url(self.request.uri)

        template_values['is_mobile'] = False
        template_values['is_mobile_capable'] = False
        template_values['is_ipad'] = False

        if self.is_mobile_capable():
            template_values['is_mobile_capable'] = True
            template_values['is_ipad'] = self.is_ipad()

            if 'is_mobile_allowed' in template_values and template_values['is_mobile_allowed']:
                template_values['is_mobile'] = self.is_mobile()

        # overridable hide_analytics querystring that defaults to true in dev
        # mode but false for prod.
        hide_analytics = self.request_bool("hide_analytics", App.is_dev_server)
        template_values['hide_analytics'] = hide_analytics

        # client-side error logging
        template_values['include_errorception'] = gandalf('errorception')

        # Analytics
        template_values['mixpanel_enabled'] = gandalf('mixpanel_enabled')

        if False: # Enable for testing only
            template_values['mixpanel_test'] = "70acc4fce4511b89477ac005639cfee1"
            template_values['mixpanel_enabled'] = True
            template_values['hide_analytics'] = False

        if template_values['mixpanel_enabled']:
            template_values['mixpanel_id'] = gae_bingo.identity.identity()

        if not template_values['hide_analytics']:
            superprops_list = user_models.UserData.get_analytics_properties(user_data)

            # Create a superprops dict for MixPanel with a version number
            # Bump the version number if changes are made to the client-side analytics
            # code and we want to be able to filter by version.
            template_values['mixpanel_superprops'] = dict(superprops_list)

            # Copy over first 4 per-user properties for GA (5th is reserved for Bingo)
            template_values['ga_custom_vars'] = superprops_list[0:4]

        if user_data:
            user_goals = goals.models.GoalList.get_current_goals(user_data)
            goals_data = [g.get_visible_data() for g in user_goals]
            if goals_data:
                template_values['global_goals'] = jsonify(goals_data)

        # Disable topic browser in the header on mobile devices
        template_values['watch_topic_browser_enabled'] = not self.is_mobile_capable()

        template_values['show_topic_pages'] = True

        return template_values
    def add_global_template_values(self, template_values):
        template_values['App'] = App
        template_values['None'] = None

        if not template_values.has_key('user_data'):
            user_data = user_models.UserData.current()
            template_values['user_data'] = user_data

        user_data = template_values['user_data']

        template_values['username'] = user_data.nickname if user_data else ""
        template_values['viewer_profile_root'] = user_data.profile_root if user_data else "/profile/nouser/"
        template_values['points'] = user_data.points if user_data else 0
        template_values['logged_in'] = not user_data.is_phantom if user_data else False
        template_values['http_host'] = os.environ["HTTP_HOST"]

        # Always insert a post-login request before our continue url
        template_values['continue'] = util.create_post_login_url(template_values.get('continue') or self.request.uri)
        template_values['login_url'] = ('%s&direct=1' % util.create_login_url(template_values['continue']))
        template_values['logout_url'] = util.create_logout_url(self.request.uri)

        template_values['is_mobile'] = False
        template_values['is_mobile_capable'] = False
        template_values['is_ipad'] = False

        if self.is_mobile_capable():
            template_values['is_mobile_capable'] = True
            template_values['is_ipad'] = self.is_ipad()

            if 'is_mobile_allowed' in template_values and template_values['is_mobile_allowed']:
                template_values['is_mobile'] = self.is_mobile()

        # overridable hide_analytics querystring that defaults to true in dev
        # mode but false for prod.
        hide_analytics = self.request_bool("hide_analytics", App.is_dev_server)
        template_values['hide_analytics'] = hide_analytics

        # client-side error logging
        template_values['include_errorception'] = gandalf('errorception')

        # Analytics
        template_values['mixpanel_enabled'] = gandalf('mixpanel_enabled')

        if False: # Enable for testing only
            template_values['mixpanel_test'] = "70acc4fce4511b89477ac005639cfee1"
            template_values['mixpanel_enabled'] = True
            template_values['hide_analytics'] = False

        if template_values['mixpanel_enabled']:
            template_values['mixpanel_id'] = gae_bingo.identity.identity()

        if not template_values['hide_analytics']:
            superprops_list = user_models.UserData.get_analytics_properties(user_data)

            # Create a superprops dict for MixPanel with a version number
            # Bump the version number if changes are made to the client-side analytics
            # code and we want to be able to filter by version.
            template_values['mixpanel_superprops'] = dict(superprops_list)

            # Copy over first 4 per-user properties for GA (5th is reserved for Bingo)
            template_values['ga_custom_vars'] = superprops_list[0:4]

        if user_data:
            user_goals = goals.models.GoalList.get_current_goals(user_data)
            goals_data = [g.get_visible_data() for g in user_goals]
            if goals_data:
                template_values['global_goals'] = jsonify(goals_data)

        # Disable topic browser in the header on mobile devices
        template_values['watch_topic_browser_enabled'] = not self.is_mobile_capable()

        # Begin topic pages A/B test
        if template_values['mixpanel_enabled']:
            show_topic_pages = ab_test("Show topic pages", ["show", "hide"], ["topic_pages_view_page", "topic_pages_started_video", "topic_pages_completed_video"])
            analytics_bingo = {"name": "Bingo: Topic pages", "value": show_topic_pages}
            template_values['analytics_bingo'] = analytics_bingo
        else:
            show_topic_pages = "hide"
        template_values['show_topic_pages'] = (show_topic_pages == "show")
        # End topic pages A/B test

        return template_values
Exemple #15
0
    def can_cross_empty_bridge(self):
        bridge_name = "balrog"

        GandalfBridge.get_or_insert(bridge_name)

        return gandalf(bridge_name)
Exemple #16
0
    def get(self, exid=None):

        # TODO(david): Is there some webapp2 magic that will allow me not to
        #     repeat this URL string in main.py?
        review_mode = self.request.path == "/review" and (ab_test(
            'Review Mode UI',
            conversion_name=ViewExercise._review_conversion_names,
            conversion_type=ViewExercise._review_conversion_types))

        if not exid and not review_mode:
            self.redirect("/exercise/%s" %
                          self.request_string("exid", default="addition_1"))
            return

        user_data = models.UserData.current() or models.UserData.pre_phantom()
        user_exercise_graph = models.UserExerciseGraph.get(user_data)

        if review_mode:
            # Take the first review exercise if available
            exid = (user_exercise_graph.review_exercise_names()
                    or user_exercise_graph.proficient_exercise_names()
                    or ["addition_1"])[0]
            reviews_left_count = user_exercise_graph.reviews_left_count()

        exercise = models.Exercise.get_by_name(exid)

        if not exercise:
            raise MissingExerciseException("Missing exercise w/ exid '%s'" %
                                           exid)

        user_exercise = user_data.get_or_insert_exercise(exercise)

        # Cache these so we don't have to worry about future lookups
        user_exercise.exercise_model = exercise
        user_exercise._user_data = user_data
        user_exercise._user_exercise_graph = user_exercise_graph
        user_exercise.summative = exercise.summative

        # Temporarily work around in-app memory caching bug
        exercise.user_exercise = None

        problem_number = self.request_int('problem_number',
                                          default=(user_exercise.total_done +
                                                   1))

        user_data_student = self.request_student_user_data(
            legacy=True) or user_data
        if user_data_student.key_email != user_data.key_email and not user_data_student.is_visible_to(
                user_data):
            user_data_student = user_data

        viewing_other = user_data_student.key_email != user_data.key_email

        # Can't view your own problems ahead of schedule
        if not viewing_other and problem_number > user_exercise.total_done + 1:
            problem_number = user_exercise.total_done + 1

        # When viewing another student's problem or a problem out-of-order, show read-only view
        read_only = viewing_other or problem_number != (
            user_exercise.total_done + 1)

        # [Multilingual]
        ML_language = (
            ':' in exid) and exid.split(':')[0] or 'en'  # Default to english
        ML_exercise = (':' in exid) and exid.split(':')[1] or exid
        exercise_template_html = exercise_template(ML_language)
        exercise_template_html = exercise_template_html.decode('utf-8')

        exercise_body_html, exercise_inline_script, exercise_inline_style, data_require, sha1 = exercise_contents(
            ML_exercise, ML_language)
        user_exercise.exercise_model.sha1 = sha1

        user_exercise.exercise_model.related_videos = map(
            lambda exercise_video: exercise_video.video,
            user_exercise.exercise_model.related_videos_fetch())
        for video in user_exercise.exercise_model.related_videos:
            video.id = video.key().id()

        renderable = True

        if read_only:
            # Override current problem number and user being inspected
            # so proper exercise content will be generated
            user_exercise.total_done = problem_number - 1
            user_exercise.user = user_data_student.user
            user_exercise.read_only = True

            if not self.request_bool("renderable", True):
                # We cannot render old problems that were created in the v1 exercise framework.
                renderable = False

            query = models.ProblemLog.all()
            query.filter("user = "******"exercise = ", exid)

            # adding this ordering to ensure that query is served by an existing index.
            # could be ok if we remove this
            query.order('time_done')
            problem_logs = query.fetch(500)

            problem_log = None
            for p in problem_logs:
                if p.problem_number == problem_number:
                    problem_log = p
                    break

            user_activity = []
            previous_time = 0

            if not problem_log or not hasattr(problem_log,
                                              "hint_after_attempt_list"):
                renderable = False
            else:
                # Don't include incomplete information
                problem_log.hint_after_attempt_list = filter(
                    lambda x: x != -1, problem_log.hint_after_attempt_list)

                while len(problem_log.hint_after_attempt_list
                          ) and problem_log.hint_after_attempt_list[0] == 0:
                    user_activity.append([
                        "hint-activity", "0",
                        max(
                            0, problem_log.hint_time_taken_list[0] -
                            previous_time)
                    ])

                    previous_time = problem_log.hint_time_taken_list[0]
                    problem_log.hint_after_attempt_list.pop(0)
                    problem_log.hint_time_taken_list.pop(0)

                # For each attempt, add it to the list and then add any hints
                # that came after it
                for i in range(0, len(problem_log.attempts)):
                    user_activity.append([
                        "correct-activity"
                        if problem_log.correct else "incorrect-activity",
                        unicode(problem_log.attempts[i] if problem_log.
                                attempts[i] else 0),
                        max(0,
                            problem_log.time_taken_attempts[i] - previous_time)
                    ])

                    previous_time = 0

                    # Here i is 0-indexed but problems are numbered starting at 1
                    while (len(problem_log.hint_after_attempt_list) and
                           problem_log.hint_after_attempt_list[0] == i + 1):
                        user_activity.append([
                            "hint-activity", "0",
                            max(
                                0, problem_log.hint_time_taken_list[0] -
                                previous_time)
                        ])

                        previous_time = problem_log.hint_time_taken_list[0]
                        # easiest to just pop these instead of maintaining
                        # another index into this list
                        problem_log.hint_after_attempt_list.pop(0)
                        problem_log.hint_time_taken_list.pop(0)

                user_exercise.user_activity = user_activity

                if problem_log.count_hints is not None:
                    user_exercise.count_hints = problem_log.count_hints

                user_exercise.current = problem_log.sha1 == sha1

        is_webos = self.is_webos()
        browser_disabled = is_webos or self.is_older_ie()
        renderable = renderable and not browser_disabled

        url_pattern = "/exercise/%s?student_email=%s&problem_number=%d"
        user_exercise.previous_problem_url = url_pattern % \
            (exid, user_data_student.key_email, problem_number - 1)
        user_exercise.next_problem_url = url_pattern % \
            (exid, user_data_student.key_email, problem_number + 1)

        user_exercise_json = jsonify.jsonify(user_exercise)

        template_values = {
            'exercise':
            exercise,
            'user_exercise_json':
            user_exercise_json,
            'exercise_body_html':
            exercise_body_html,
            'exercise_template_html':
            exercise_template_html,
            'exercise_inline_script':
            exercise_inline_script,
            'exercise_inline_style':
            exercise_inline_style,
            'data_require':
            data_require,
            'read_only':
            read_only,
            'selected_nav_link':
            'practice',
            'browser_disabled':
            browser_disabled,
            'is_webos':
            is_webos,
            'renderable':
            renderable,
            'issue_labels':
            ('Component-Code,Exercise-%s,Problem-%s' % (exid, problem_number)),
            'alternate_hints_treatment':
            ab_test('Hints or Show Solution Dec 10',
                    ViewExercise._hints_ab_test_alternatives,
                    ViewExercise._hints_conversion_names,
                    ViewExercise._hints_conversion_types,
                    'Hints or Show Solution Nov 5'),
            'reviews_left_count':
            reviews_left_count if review_mode else "null",
            'include_errorception':
            gandalf("errorception"),
        }

        self.render_jinja2_template("exercise_template.html", template_values)