Example #1
0
 def create_random_video_file(self):
     """
     Helper function for testing video files.
     """
     video_id = get_node_cache("Video").keys()[0]
     youtube_id = get_node_cache("Video")[video_id][0]["youtube_id"]
     fake_video_file = os.path.join(settings.CONTENT_ROOT, "%s.mp4" % youtube_id)
     with open(fake_video_file, "w") as fh:
         fh.write("")
     self.assertTrue(os.path.exists(fake_video_file), "Make sure the video file was created, youtube_id='%s'." % youtube_id)
     return (fake_video_file, video_id, youtube_id)
Example #2
0
def exercise_dashboard(request):
    slug = request.GET.get("topic")
    if not slug:
        title = _("Your Knowledge Map")
    elif slug in topic_tools.get_node_cache("Topic"):
        title = _(topic_tools.get_node_cache("Topic")[slug][0]["title"])
    else:
        raise Http404

    context = {
        "title": title,
    }
    return context
Example #3
0
 def get_playlist_entry_ids(cls, playlist):
     """Return a tuple of the playlist's video ids and exercise ids as sets"""
     playlist_entries = playlist.get("children")
     topic_cache = get_node_cache()["Topic"]
     pl_video_ids = set([id for id in playlist_entries if topic_cache.get(id).get("kind") == "Video"])
     pl_exercise_ids = set([id for id in playlist_entries if topic_cache.get(id).get("kind") == "Exercise"])
     return (pl_video_ids, pl_exercise_ids)
Example #4
0
    def handle(self, *args, **options):
        if not options["lang_code"]:
            raise CommandError("You must specify a language code.")

        lang_code = lcode_to_ietf(options["lang_code"])
        if lang_code not in AVAILABLE_EXERCISE_LANGUAGE_CODES:
            logging.info("No exercises available for language %s" % lang_code)

        else:
            # Get list of exercises
            exercise_ids = options["exercise_ids"].split(
                ",") if options["exercise_ids"] else None
            exercise_ids = exercise_ids or ([
                ex["id"]
                for ex in get_topic_exercises(topic_id=options["topic_id"])
            ] if options["topic_id"] else None)
            exercise_ids = exercise_ids or get_node_cache("Exercise").keys()

            # Download the exercises
            for exercise_id in exercise_ids:
                scrape_exercise(exercise_id=exercise_id,
                                lang_code=lang_code,
                                force=options["force"])

        logging.info("Process complete.")
Example #5
0
def update_all_distributed_callback(request):
    """
    """

    if request.method != "POST":
        raise PermissionDenied("Only POST allowed to this URL endpoint.")

    videos = json.loads(request.POST["video_logs"])
    exercises = json.loads(request.POST["exercise_logs"])
    user = FacilityUser.objects.get(id=request.POST["user_id"])
    node_cache = get_node_cache()
    # Save videos
    n_videos_uploaded = 0
    for video in videos:
        video_id = video['video_id']
        youtube_id = video['youtube_id']

        # Only save video logs for videos that we recognize.
        if video_id not in node_cache["Content"]:
            logging.warn("Skipping unknown video %s" % video_id)
            continue

        try:
            (vl, _) = VideoLog.get_or_initialize(user=user, video_id=video_id)  # has to be that video_id, could be any youtube_id
            for key,val in video.iteritems():
                setattr(vl, key, val)
            logging.debug("Saving video log for %s: %s" % (video_id, vl))
            vl.save()
            n_videos_uploaded += 1
        except KeyError:  #
            logging.error("Could not save video log for data with missing values: %s" % video)
        except Exception as e:
            error_message = _("Unexpected error importing videos: %(err_msg)s") % {"err_msg": e}
            return JsonResponseMessageError(error_message)

    # Save exercises
    n_exercises_uploaded = 0
    for exercise in exercises:
        # Only save video logs for videos that we recognize.
        if exercise['exercise_id'] not in node_cache['Exercise']:
            logging.warn("Skipping unknown video %s" % exercise['exercise_id'])
            continue

        try:
            (el, _) = ExerciseLog.get_or_initialize(user=user, exercise_id=exercise["exercise_id"])
            for key,val in exercise.iteritems():
                setattr(el, key, val)
            logging.debug("Saving exercise log for %s: %s" % (exercise['exercise_id'], el))
            el.save()
            n_exercises_uploaded += 1
        except KeyError:
            logging.error("Could not save exercise log for data with missing values: %s" % exercise)
        except Exception as e:
            error_message = _("Unexpected error importing exercises: %(err_msg)s") % {"err_msg": e}
            return JsonResponseMessageError(error_message)

    return JsonResponseMessageSuccess(_("Uploaded %(num_exercises)d exercises and %(num_videos)d videos") % {
        "num_exercises": n_exercises_uploaded,
        "num_videos": n_videos_uploaded,
    })
Example #6
0
def update_all_distributed_callback(request):
    """
    """

    if request.method != "POST":
        raise PermissionDenied("Only POST allowed to this URL endpoint.")

    videos = json.loads(request.POST["video_logs"])
    exercises = json.loads(request.POST["exercise_logs"])
    user = FacilityUser.objects.get(id=request.POST["user_id"])
    node_cache = get_node_cache()
    # Save videos
    n_videos_uploaded = 0
    for video in videos:
        video_id = video['video_id']
        youtube_id = video['youtube_id']

        # Only save video logs for videos that we recognize.
        if video_id not in node_cache["Video"]:
            logging.warn("Skipping unknown video %s" % video_id)
            continue

        try:
            (vl, _) = VideoLog.get_or_initialize(user=user, video_id=video_id)  # has to be that video_id, could be any youtube_id
            for key,val in video.iteritems():
                setattr(vl, key, val)
            logging.debug("Saving video log for %s: %s" % (video_id, vl))
            vl.save()
            n_videos_uploaded += 1
        except KeyError:  #
            logging.error("Could not save video log for data with missing values: %s" % video)
        except Exception as e:
            error_message = _("Unexpected error importing videos: %(err_msg)s") % {"err_msg": e}
            return JsonResponseMessageError(error_message)

    # Save exercises
    n_exercises_uploaded = 0
    for exercise in exercises:
        # Only save video logs for videos that we recognize.
        if exercise['exercise_id'] not in node_cache['Exercise']:
            logging.warn("Skipping unknown video %s" % exercise['exercise_id'])
            continue

        try:
            (el, _) = ExerciseLog.get_or_initialize(user=user, exercise_id=exercise["exercise_id"])
            for key,val in exercise.iteritems():
                setattr(el, key, val)
            logging.debug("Saving exercise log for %s: %s" % (exercise['exercise_id'], el))
            el.save()
            n_exercises_uploaded += 1
        except KeyError:
            logging.error("Could not save exercise log for data with missing values: %s" % exercise)
        except Exception as e:
            error_message = _("Unexpected error importing exercises: %(err_msg)s") % {"err_msg": e}
            return JsonResponseMessageError(error_message)

    return JsonResponseMessageSuccess(_("Uploaded %(num_exercises)d exercises and %(num_videos)d videos") % {
        "num_exercises": n_exercises_uploaded,
        "num_videos": n_videos_uploaded,
    })
Example #7
0
    def set_unit_navigate_to_exercise(self, unit, exercise_id):
        """
        Set the student unit. Navigate to an exercise.
        """

        set_current_unit_settings_value(self.facility.id, unit)
        self.browse_to(self.live_server_url + get_node_cache("Exercise")[exercise_id]["path"])
Example #8
0
 def clean_exercise_id(self):
     """
     Make sure the exercise ID is found.
     """
     if not self.cleaned_data.get("exercise_id",
                                  "") in get_node_cache('Exercise'):
         raise forms.ValidationError(_("Exercise ID not recognized"))
Example #9
0
    def set_unit_navigate_to_exercise(self, unit, exercise_id):
        """
        Set the student unit. Navigate to an exercise.
        """

        set_current_unit_settings_value(self.facility.id, unit)
        self.browse_to(self.live_server_url +
                       get_node_cache("Exercise")[exercise_id]["path"])
Example #10
0
    def show_cache(self, force=False):
        """Go through each cacheable page, and show which are cached and which are NOT"""

        for node_type in ['Topic', 'Video', 'Exercise']:
            self.stdout.write("Cached %ss:\n" % node_type)
            for narr in topic_tools.get_node_cache(node_type).values():
                for n in narr:
                    if caching.has_cache_key(path=n["path"]):
                        self.stdout.write("\t%s\n" % n["path"])
Example #11
0
    def show_cache(self, force=False):
        """Go through each cacheable page, and show which are cached and which are NOT"""

        for node_type in ['Topic', 'Content', 'Exercise']:
            self.stdout.write("Cached %ss:\n" % node_type)
            for narr in topic_tools.get_node_cache(node_type).values():
                for n in narr:
                    if caching.has_cache_key(path=n["path"]):
                        self.stdout.write("\t%s\n" % n["path"])
Example #12
0
    def test_topic_availability(self):

        for node_list in get_node_cache("Topic").values():
            for topic in node_list:
                if "Exercise" in topic["contains"]:
                    self.assertTrue(topic["available"], "Make sure all topics containing exercises are shown as available.")
                if topic["children"] and len(topic["contains"]) == 1 and "Video" in topic["contains"]:
                    any_on_disk = bool(sum([v["on_disk"] for v in topic["children"]]))
                    self.assertEqual(topic["available"], any_on_disk, "Make sure topic availability matches video availability when only videos are available.")
Example #13
0
def knowledge_map_json(request, topic_id):
    """
    Topic nodes can now have a "knowledge_map" stamped on them.
    This code currently exposes that data to the kmap-editor code,
    mostly as it expects it now.

    So this is kind of a hack-ish mix of code that avoids rewriting kmap-editor.js,
    but allows a cleaner rewrite of the stored data, and bridges the gap between
    that messiness and the cleaner back-end.
    """

    # Try and get the requested topic, and make sure it has knowledge map data available.
    topic = get_node_cache("Topic").get(topic_id)
    if not topic:
        raise Http404("Topic '%s' not found" % topic_id)
    elif not "knowledge_map" in topic[0]:
        raise Http404("Topic '%s' has no knowledge map metadata." % topic_id)

    # For each node (can be of any type now), pull out only
    #   the relevant data.
    kmap = topic[0]["knowledge_map"]
    nodes_out = {}
    for id, kmap_data in kmap["nodes"].iteritems():
        cur_node = get_node_cache(kmap_data["kind"])[id][0]
        nodes_out[id] = {
            "id": cur_node["id"],
            "title": _(cur_node["title"]),
            "h_position": kmap_data["h_position"],
            "v_position": kmap_data["v_position"],
            "icon_url": cur_node.get("icon_url",
                                     cur_node.get("icon_src")),  # messy
            "path": cur_node["path"],
        }
        if not "polylines" in kmap:  # messy
            # Two ways to define lines:
            # 1. have "polylines" defined explicitly
            # 2. use prerequisites to compute lines on the fly.
            nodes_out[id]["prerequisites"] = cur_node.get("prerequisites", [])

    return JsonResponse({
        "nodes": nodes_out,
        "polylines": kmap.get("polylines"),  # messy
    })
Example #14
0
    def clear_cache(self):
        """Go through each cacheable page, and show which are cached and which are NOT"""

        for node_type in ['Topic', 'Content', 'Exercise']:
            self.stdout.write("Clearing %ss:\n" % node_type)
            for narr in topic_tools.get_node_cache(node_type).values():
                for n in narr:
                    if caching.has_cache_key(path=n["path"]):
                        self.stdout.write("\t%s\n" % n["path"])
                        caching.expire_page(path=n["path"])
Example #15
0
    def setUp(self):
        """
        Create a student, log the student in, and go to the exercise page.
        """
        super(StudentExerciseTest, self).setUp()
        self.student = self.create_student(facility_name=self.facility_name)
        self.browser_login_student(self.student_username, self.student_password, facility_name=self.facility_name)

        self.browse_to(self.live_server_url + get_node_cache("Exercise")[self.EXERCISE_SLUG][0]["path"])
        self.browser_check_django_message(num_messages=0)  # make sure no messages
Example #16
0
    def clear_cache(self):
        """Go through each cacheable page, and show which are cached and which are NOT"""

        for node_type in ['Topic', 'Video', 'Exercise']:
            self.stdout.write("Clearing %ss:\n" % node_type)
            for narr in topic_tools.get_node_cache(node_type).values():
                for n in narr:
                    if caching.has_cache_key(path=n["path"]):
                        self.stdout.write("\t%s\n" % n["path"])
                        caching.expire_page(path=n["path"])
Example #17
0
def knowledge_map_json(request, topic_id):
    """
    Topic nodes can now have a "knowledge_map" stamped on them.
    This code currently exposes that data to the kmap-editor code,
    mostly as it expects it now.

    So this is kind of a hack-ish mix of code that avoids rewriting kmap-editor.js,
    but allows a cleaner rewrite of the stored data, and bridges the gap between
    that messiness and the cleaner back-end.
    """

    # Try and get the requested topic, and make sure it has knowledge map data available.
    topic = get_node_cache("Topic").get(topic_id)
    if not topic:
        raise Http404("Topic '%s' not found" % topic_id)
    elif not "knowledge_map" in topic[0]:
        raise Http404("Topic '%s' has no knowledge map metadata." % topic_id)

    # For each node (can be of any type now), pull out only
    #   the relevant data.
    kmap = topic[0]["knowledge_map"]
    nodes_out = {}
    for id, kmap_data in kmap["nodes"].iteritems():
        cur_node = get_node_cache(kmap_data["kind"])[id][0]
        nodes_out[id] = {
            "id": cur_node["id"],
            "title": _(cur_node["title"]),
            "h_position":  kmap_data["h_position"],
            "v_position": kmap_data["v_position"],
            "icon_url": cur_node.get("icon_url", cur_node.get("icon_src")),  # messy
            "path": cur_node["path"],
        }
        if not "polylines" in kmap:  # messy
            # Two ways to define lines:
            # 1. have "polylines" defined explicitly
            # 2. use prerequisites to compute lines on the fly.
            nodes_out[id]["prerequisites"] = cur_node.get("prerequisites", [])

    return JsonResponse({
        "nodes": nodes_out,
        "polylines": kmap.get("polylines"),  # messy
    })
Example #18
0
def save_exercise_log(request):
    """
    Receives an exercise_id and relevant data,
    saves it to the currently authorized user.
    """

    # Form does all data validation, including of the exercise_id
    form = ExerciseLogForm(data=simplejson.loads(request.body))
    if not form.is_valid():
        raise Exception(form.errors)
    data = form.data

    # More robust extraction of previous object
    user = request.session["facility_user"]
    (exerciselog, was_created) = ExerciseLog.get_or_initialize(
        user=user, exercise_id=data["exercise_id"])
    previously_complete = exerciselog.complete

    exerciselog.attempts = data[
        "attempts"]  # don't increment, because we fail to save some requests
    exerciselog.streak_progress = data["streak_progress"]
    exerciselog.points = data["points"]
    exerciselog.language = data.get("language") or request.language

    try:
        exerciselog.full_clean()
        exerciselog.save(update_userlog=True)
    except ValidationError as e:
        return JsonResponseMessageError(
            _("Could not save ExerciseLog") + u": %s" % e)

    if "points" in request.session:
        del request.session["points"]  # will be recomputed when needed

    # Special message if you've just completed.
    #   NOTE: it's important to check this AFTER calling save() above.
    if not previously_complete and exerciselog.complete:
        exercise = get_node_cache("Exercise").get(data["exercise_id"],
                                                  [None])[0]
        junk, next_exercise = get_neighbor_nodes(
            exercise, neighbor_kind="Exercise") if exercise else None
        if not next_exercise:
            return JsonResponseMessageSuccess(
                _("You have mastered this exercise and this topic!"))
        else:
            return JsonResponseMessageSuccess(
                _("You have mastered this exercise!  Please continue on to <a href='%(href)s'>%(title)s</a>"
                  ) % {
                      "href": next_exercise["path"],
                      "title": _(next_exercise["title"]),
                  })

    # Return no message in release mode; "data saved" message in debug mode.
    return JsonResponse({})
Example #19
0
def search(request):
    # Inputs
    query = request.GET.get('query')
    category = request.GET.get('category')
    max_results_per_category = request.GET.get('max_results', 25)

    # Outputs
    query_error = None
    possible_matches = {}
    hit_max = {}

    if query is None:
        query_error = _("Error: query not specified.")

#    elif len(query) < 3:
#        query_error = _("Error: query too short.")

    else:
        query = query.lower()
        # search for topic, video or exercise with matching title
        nodes = []
        for node_type, node_dict in topic_tools.get_node_cache().iteritems():
            if category and node_type != category:
                # Skip categories that don't match (if specified)
                continue

            possible_matches[node_type] = []  # make dict only for non-skipped categories
            for node in node_dict.values():
                title = _(node['title']).lower()  # this could be done once and stored.
                keywords = [x.lower() for x in node.get('keywords', [])]
                tags = [x.lower() for x in node.get('tags', [])]
                if title == query:
                    # Redirect to an exact match
                    return HttpResponseRedirect(reverse('learn') + node['path'])

                elif (len(possible_matches[node_type]) < max_results_per_category and
                    (query in title or query in keywords or query in tags)):
                    # For efficiency, don't do substring matches when we've got lots of results
                    possible_matches[node_type].append(node)


            hit_max[node_type] = len(possible_matches[node_type]) == max_results_per_category

    return {
        'title': _("Search results for '%(query)s'") % {"query": (query if query else "")},
        'query_error': query_error,
        'results': possible_matches,
        'hit_max': hit_max,
        'query': query,
        'max_results': max_results_per_category,
        'category': category,
    }
Example #20
0
 def get_playlist_entries(playlist, entry_type, language=settings.LANGUAGE_CODE):
     """
     Given a VanillaPlaylist, inspect its 'entries' attribute and return a list
     containing corresponding nodes for each item from the topic tree.
     entry_type should be "Exercise" or "Video".
     """
     unprepared = filter(lambda e: e["entity_kind"] == entry_type, playlist.entries)
     prepared = []
     for entry in unprepared:
         new_item = get_node_cache(language=language)[entry_type].get(entry["entity_id"], None)
         if new_item:
             prepared.append(new_item)
     return prepared
Example #21
0
 def get_playlist_entries(playlist, entry_type, language=settings.LANGUAGE_CODE):
     """
     Given a VanillaPlaylist, inspect its 'entries' attribute and return a list
     containing corresponding nodes for each item from the topic tree.
     entry_type should be "Exercise" or "Video".
     """
     unprepared = filter(lambda e: e["entity_kind"]==entry_type, playlist.entries)
     prepared = []
     for entry in unprepared:
         new_item = get_node_cache(language=language)[entry_type].get(entry['entity_id'], None)
         if new_item:
             prepared.append(new_item)
     return prepared
Example #22
0
 def get_playlist_entry_ids(cls, playlist):
     """Return a tuple of the playlist's video ids and exercise ids as sets"""
     playlist_entries = playlist.get("children")
     topic_cache = get_node_cache()["Topic"]
     pl_video_ids = set([
         id for id in playlist_entries
         if topic_cache.get(id).get("kind") == "Video"
     ])
     pl_exercise_ids = set([
         id for id in playlist_entries
         if topic_cache.get(id).get("kind") == "Exercise"
     ])
     return (pl_video_ids, pl_exercise_ids)
Example #23
0
    def setUp(self):
        """
        Create a student, log the student in, and go to the exercise page.
        """
        super(StudentExerciseTest, self).setUp()
        self.facility_name = "fac"
        self.facility = self.create_facility(name=self.facility_name)
        self.student = self.create_student(username=self.student_username,
                                           password=self.student_password,
                                           facility=self.facility)
        self.browser_login_student(self.student_username, self.student_password, facility_name=self.facility_name)

        self.browse_to(self.live_server_url + reverse("learn") + get_node_cache("Exercise")[self.EXERCISE_SLUG]["path"])
        self.nanswers = self.browser.execute_script('return window.ExerciseParams.STREAK_CORRECT_NEEDED;')
Example #24
0
    def setUp(self):
        """
        Create a student, log the student in, and go to the exercise page.
        """
        super(StudentExerciseTest, self).setUp()
        self.facility_name = "fac"
        self.facility = self.create_facility(name=self.facility_name)
        self.student = self.create_student(
            username=self.student_username, password=self.student_password, facility=self.facility
        )
        self.browser_login_student(self.student_username, self.student_password, facility_name=self.facility_name)

        self.browse_to(self.live_server_url + reverse("learn") + get_node_cache("Exercise")[self.EXERCISE_SLUG]["path"])
        self.nanswers = self.browser.execute_script("return window.ExerciseParams.STREAK_CORRECT_NEEDED;")
Example #25
0
 def test_topic_availability(self):
     for topic in get_node_cache("Topic").values():
         if "Exercise" in topic["contains"]:
             self.assertTrue(
                 topic["available"],
                 "Make sure all topics containing exercises are shown as available."
             )
         if topic["children"] and "Video" in topic["contains"]:
             any_available = bool(
                 sum([v.get("available", False)
                      for v in topic["children"]]))
             self.assertEqual(
                 topic["available"], any_available,
                 "Make sure topic availability matches video availability when only videos are available."
             )
Example #26
0
    def setUp(self):
        """
        Create a student, log the student in, and go to the exercise page.
        """
        super(StudentExerciseTest, self).setUp()
        self.student = self.create_student(facility_name=self.facility_name)
        self.browser_login_student(self.student_username,
                                   self.student_password,
                                   facility_name=self.facility_name)

        self.browse_to(
            self.live_server_url +
            get_node_cache("Exercise")[self.EXERCISE_SLUG][0]["path"])
        self.browser_check_django_message(
            num_messages=0)  # make sure no messages
Example #27
0
def save_exercise_log(request):
    """
    Receives an exercise_id and relevant data,
    saves it to the currently authorized user.
    """

    # Form does all data validation, including of the exercise_id
    form = ExerciseLogForm(data=simplejson.loads(request.body))
    if not form.is_valid():
        raise Exception(form.errors)
    data = form.data

    # More robust extraction of previous object
    user = request.session["facility_user"]
    (exerciselog, was_created) = ExerciseLog.get_or_initialize(user=user, exercise_id=data["exercise_id"])
    previously_complete = exerciselog.complete

    exerciselog.attempts = data["attempts"]  # don't increment, because we fail to save some requests
    exerciselog.streak_progress = data["streak_progress"]
    exerciselog.points = data["points"]
    exerciselog.language = data.get("language") or request.language

    try:
        exerciselog.full_clean()
        exerciselog.save(update_userlog=True)
    except ValidationError as e:
        return JsonResponseMessageError(_("Could not save ExerciseLog") + u": %s" % e)

    if "points" in request.session:
        del request.session["points"]  # will be recomputed when needed

    # Special message if you've just completed.
    #   NOTE: it's important to check this AFTER calling save() above.
    if not previously_complete and exerciselog.complete:
        exercise = get_node_cache("Exercise").get(data["exercise_id"], [None])[0]
        junk, next_exercise = get_neighbor_nodes(exercise, neighbor_kind="Exercise") if exercise else None
        if not next_exercise:
            return JsonResponseMessageSuccess(_("You have mastered this exercise and this topic!"))
        else:
            return JsonResponseMessageSuccess(_("You have mastered this exercise!  Please continue on to <a href='%(href)s'>%(title)s</a>") % {
                "href": next_exercise["path"],
                "title": _(next_exercise["title"]),
            })

    # Return no message in release mode; "data saved" message in debug mode.
    return JsonResponse({})
    def handle(self, *args, **options):
        if not options["lang_code"]:
            raise CommandError("You must specify a language code.")


        lang_code = lcode_to_ietf(options["lang_code"])
        if lang_code not in AVAILABLE_EXERCISE_LANGUAGE_CODES:
            logging.info("No exercises available for language %s" % lang_code)

        else:
            # Get list of exercises
            exercise_ids = options["exercise_ids"].split(",") if options["exercise_ids"] else None
            exercise_ids = exercise_ids or ([ex["id"] for ex in get_topic_exercises(topic_id=options["topic_id"])] if options["topic_id"] else None)
            exercise_ids = exercise_ids or get_node_cache("Exercise").keys()

            # Download the exercises
            for exercise_id in exercise_ids:
                scrape_exercise(exercise_id=exercise_id, lang_code=lang_code, force=options["force"])

        logging.info("Process complete.")
Example #29
0
    def _setup(self, num_logs=50, **kwargs):
        super(OneHundredRandomLogUpdates, self)._setup(**kwargs)
        node_cache = get_node_cache()

        try:
            self.user = FacilityUser.objects.get(username=self.username)
        except:
            #take username from ExerciseLog
            all_exercises = ExerciseLog.objects.all()
            self.user = FacilityUser.objects.get(id=all_exercises[0].user_id)
            print self.username, " not in FacilityUsers, using ", self.user
        self.num_logs = num_logs
        #give the platform a chance to cache the logs
        ExerciseLog.objects.filter(user=self.user).delete()
        for x in range(num_logs):
            while True:
                ex_idx = int(self.random.random() *
                             len(node_cache["Exercise"].keys()))
                ex_id = node_cache["Exercise"].keys()[ex_idx]
                if not ExerciseLog.objects.filter(user=self.user,
                                                  exercise_id=ex_id):
                    break
            ex = ExerciseLog(user=self.user, exercise_id=ex_id)
            ex.save()
        self.exercise_list = ExerciseLog.objects.filter(user=self.user)
        self.exercise_count = self.exercise_list.count()

        VideoLog.objects.filter(user=self.user).delete()
        for x in range(num_logs):
            while True:
                vid_idx = int(self.random.random() *
                              len(node_cache["Content"].keys()))
                vid_id = node_cache["Content"].keys()[vid_idx]
                if not VideoLog.objects.filter(user=self.user,
                                               video_id=vid_id):
                    break
            vid = VideoLog(user=self.user, video_id=vid_id)
            vid.save()
        self.video_list = VideoLog.objects.filter(user=self.user)
        self.video_count = self.video_list.count()
Example #30
0
    def handle(self, *args, **kwargs):

        MALFORMED_IDS = []

        video_slugs = set(video_dict_by_video_id().keys())
        exercise_slugs = set(get_node_cache()["Exercise"].keys())

        all_playlists = json.load(open(os.path.join(settings.PROJECT_PATH, 'playlist/playlists.json')))

        # for pl in Playlist.all():
        for pl in all_playlists:
            # entries = pl.entries
            entries = pl.get("entries")

            # Find video ids in the playlists that are not in the topic tree
            video_entry_slugs = [enforce_and_strip_slug(pl.get("id"), e['entity_id']) for e in entries if e['entity_kind'] == 'Video']
            nonexistent_video_slugs = set(filter(None, video_entry_slugs)) - video_slugs

            # Find exercise ids in the playlists that are not in the topic tree
            ex_entry_slugs = [enforce_and_strip_slug(pl.get("id"), e['entity_id']) for e in entries if e['entity_kind'] == 'Exercise']
            nonexistent_ex_slugs = set(filter(None, ex_entry_slugs)) - exercise_slugs

            # Print malformed videos
            for slug in nonexistent_video_slugs:
                errormsg = "Video slug in playlist {0} not found in videos: {1}"
                # print errormsg.format(pl.id, slug)
                print errormsg.format(pl.get("id"), slug)

            # Print malformed exercises
            for slug in nonexistent_ex_slugs:
                errormsg = "Exercise slug in playlist {0} not found in exercises: {1}"
                # print errormsg.format(pl.id, slug)
                print errormsg.format(pl.get("id"), slug)

            # Print misspelled ids
            for m in MALFORMED_IDS:
                errormsg = "Malformed slug in playlist {0}. Please investigate: {1}"
                # print errormsg.format(pl.id, slug)
                print errormsg.format(pl.get("id"), m)
            MALFORMED_IDS = []
Example #31
0
    def handle(self, *args, **options):
        if settings.CENTRAL_SERVER:
            raise CommandError("This must only be run on the distributed server.")

        if not options["lang_code"]:
            raise CommandError("You must specify a language code.")

        #
        ensure_dir(settings.CONTENT_ROOT)

        # Get list of videos
        lang_code = lcode_to_ietf(options["lang_code"])
        video_map = get_dubbed_video_map(lang_code) or {}
        video_ids = options["video_ids"].split(",") if options["video_ids"] else None
        video_ids = video_ids or (
            [vid["id"] for vid in get_topic_videos(topic_id=options["topic_id"])] if options["topic_id"] else None
        )
        video_ids = video_ids or video_map.keys()

        # Download the videos
        for video_id in video_ids:
            if video_id in video_map:
                youtube_id = video_map[video_id]

            elif video_id in video_map.values():
                # Perhaps they sent in a youtube ID?  We can handle that!
                youtube_id = video_id
            else:
                logging.error("No mapping for video_id=%s; skipping" % video_id)
                continue

            try:
                scrape_video(youtube_id=youtube_id, format=options["format"], force=options["force"])
                # scrape_thumbnail(youtube_id=youtube_id)
                logging.info("Access video %s at %s" % (youtube_id, get_node_cache("Video")[video_id][0]["path"]))
            except Exception as e:
                logging.error("Failed to download video %s: %s" % (youtube_id, e))

        logging.info("Process complete.")
Example #32
0
    def add_full_title_from_topic_tree(entry, video_title_dict):
        # TODO (aron): Add i18n by varying the language of the topic tree here
        topictree = get_node_cache()

        entry_kind = entry['entity_kind']
        entry_name = entry['entity_id']

        try:
            if entry_kind == 'Exercise':
                nodedict = topictree['Exercise']
            elif entry_kind == 'Video':
                # TODO-blocker: move use of video_dict_by_id to slug2id_map
                nodedict = video_title_dict
            else:
                nodedict = {}

            entry['title'] = nodedict[entry_name]['title']
            entry['description'] = nodedict[entry_name].get('description', '')
        except KeyError:
            # TODO: edit once we get the properly labeled entity ids from Nalanda
            entry['title'] = entry['description'] = entry['entity_id']

        return entry
    def handle(self, *args, **options):
        if settings.CENTRAL_SERVER:
            raise CommandError("This must only be run on the distributed server.")

        if not options["lang_code"]:
            raise CommandError("You must specify a language code.")

        #
        ensure_dir(settings.CONTENT_ROOT)

        # Get list of videos
        lang_code = lcode_to_ietf(options["lang_code"])
        video_map = get_dubbed_video_map(lang_code) or {}
        video_ids = options["video_ids"].split(",") if options["video_ids"] else None
        video_ids = video_ids or ([vid["id"] for vid in get_topic_videos(topic_id=options["topic_id"])] if options["topic_id"] else None)
        video_ids = video_ids or video_map.keys()

        # Download the videos
        for video_id in video_ids:
            if video_id in video_map:
                youtube_id = video_map[video_id]

            elif video_id in video_map.values():
                # Perhaps they sent in a youtube ID?  We can handle that!
                youtube_id = video_id
            else:
                logging.error("No mapping for video_id=%s; skipping" % video_id)
                continue

            try:
                scrape_video(youtube_id=youtube_id, format=options["format"], force=options["force"])
                #scrape_thumbnail(youtube_id=youtube_id)
                logging.info("Access video %s at %s" % (youtube_id, get_node_cache("Video")[video_id][0]["path"]))
            except Exception as e:
                logging.error("Failed to download video %s: %s" % (youtube_id, e))

        logging.info("Process complete.")
Example #34
0
    def _setup(self, num_logs=50, **kwargs):
        super(OneHundredRandomLogUpdates, self)._setup(**kwargs)
        node_cache = get_node_cache()

        try:
            self.user = FacilityUser.objects.get(username=self.username)
        except:
            #take username from ExerciseLog
            all_exercises = ExerciseLog.objects.all()
            self.user = FacilityUser.objects.get(id=all_exercises[0].user_id)
            print self.username, " not in FacilityUsers, using ", self.user
        self.num_logs = num_logs
        #give the platform a chance to cache the logs
        ExerciseLog.objects.filter(user=self.user).delete()
        for x in range(num_logs):
            while True:
                ex_idx = int(self.random.random() * len(node_cache["Exercise"].keys()))
                ex_id = node_cache["Exercise"].keys()[ex_idx]
                if not ExerciseLog.objects.filter(user=self.user, exercise_id=ex_id):
                    break
            ex = ExerciseLog(user=self.user, exercise_id=ex_id)
            ex.save()
        self.exercise_list = ExerciseLog.objects.filter(user=self.user)
        self.exercise_count = self.exercise_list.count()

        VideoLog.objects.filter(user=self.user).delete()
        for x in range(num_logs):
            while True:
                vid_idx = int(self.random.random() * len(node_cache["Content"].keys()))
                vid_id = node_cache["Content"].keys()[vid_idx]
                if not VideoLog.objects.filter(user=self.user, video_id=vid_id):
                    break
            vid = VideoLog(user=self.user, video_id=vid_id)
            vid.save()
        self.video_list = VideoLog.objects.filter(user=self.user)
        self.video_count = self.video_list.count()
Example #35
0
 def create_cache(self, force=False):
     for node_type in ['Topic', 'Video', 'Exercise']:
         self.stdout.write("Caching %ss:\n" % node_type)
         for narr in topic_tools.get_node_cache(node_type).values():
             for n in narr:
                 self.create_page_cache(path=n["path"], force=force)
Example #36
0
 def clean_video_id(self):
     """
     Make sure the video ID is found.
     """
     if self.cleaned_data["video_id"] not in get_node_cache("Video"):
         raise forms.ValidationError(_("Video ID not recognized."))
Example #37
0
def update_all_central_callback(request):
    """
    Callback after authentication.

    Parses out the request token verification.
    Then finishes the request by getting an auth token.
    """
    if not "ACCESS_TOKEN" in request.session:
        finish_auth(request)

    exercises = get_api_resource(request, "/api/v1/user/exercises")
    videos = get_api_resource(request, "/api/v1/user/videos")
    node_cache = get_node_cache()

    # Collate videos
    video_logs = []
    for video in videos:
        # Assume that KA videos are all english-language, not dubbed (for now)
        youtube_id = video.get('video', {}).get('youtube_id', "")
        video_id = get_video_id(youtube_id)  # map from youtube_id to video_id (across all languages)

        # Only save videos with progress
        if not video.get('seconds_watched', None):
            continue

        # Only save video logs for videos that we recognize.
        if video_id not in node_cache["Video"]:
            logging.warn("Skipping unknown video %s" % video_id)
            continue

        try:
            video_logs.append({
                "video_id": video_id,
                "youtube_id": youtube_id,
                "total_seconds_watched": video['seconds_watched'],
                "points": VideoLog.calc_points(video['seconds_watched'], video['duration']),
                "complete": video['completed'],
                "completion_timestamp": convert_ka_date(video['last_watched']) if video['completed'] else None,
            })
            logging.debug("Got video log for %s: %s" % (video_id, video_logs[-1]))
        except KeyError:  #
            logging.error("Could not save video log for data with missing values: %s" % video)

    # Collate exercises
    exercise_logs = []
    for exercise in exercises:
        # Only save exercises that have any progress.
        if not exercise.get('last_done', None):
            continue

        # Only save video logs for videos that we recognize.
        slug = exercise.get('exercise', "")
        if slug not in node_cache['Exercise']:
            logging.warn("Skipping unknown video %s" % slug)
            continue

        try:
            completed = exercise['streak'] >= 10
            basepoints = node_cache['Exercise'][slug][0]['basepoints']
            exercise_logs.append({
                "exercise_id": slug,
                "streak_progress": min(100, 100 * exercise['streak']/10),  # duplicates logic elsewhere
                "attempts": exercise['total_done'],
                "points": ExerciseLog.calc_points(basepoints, ncorrect=exercise['streak'], add_randomness=False),  # no randomness when importing from KA
                "complete": completed,
                "attempts_before_completion": exercise['total_done'] if not exercise['practiced'] else None,  #can't figure this out if they practiced after mastery.
                "completion_timestamp": convert_ka_date(exercise['proficient_date']) if completed else None,
            })
            logging.debug("Got exercise log for %s: %s" % (slug, exercise_logs[-1]))
        except KeyError:
            logging.error("Could not save exercise log for data with missing values: %s" % exercise)

    # POST the data back to the distributed server
    try:

        dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None
        logging.debug("POST'ing to %s" % request.session["distributed_callback_url"])
        response = requests.post(
            request.session["distributed_callback_url"],
            cookies={ "csrftoken": request.session["distributed_csrf_token"] },
            data = {
                "csrfmiddlewaretoken": request.session["distributed_csrf_token"],
                "video_logs": json.dumps(video_logs, default=dthandler),
                "exercise_logs": json.dumps(exercise_logs, default=dthandler),
                "user_id": request.session["distributed_user_id"],
            }
        )
        logging.debug("Response (%d): %s" % (response.status_code, response.content))
    except requests.exceptions.ConnectionError as e:
        return HttpResponseRedirect(set_query_params(request.session["distributed_redirect_url"], {
            "message_type": "error",
            "message": _("Could not connect to your KA Lite installation to share Khan Academy data."),
            "message_id": "id_khanload",
        }))
    except Exception as e:
        return HttpResponseRedirect(set_query_params(request.session["distributed_redirect_url"], {
            "message_type": "error",
            "message": _("Failure to send data to your KA Lite installation: %s") % e,
            "message_id": "id_khanload",
        }))


    try:
        json_response = json.loads(response.content)
        if not isinstance(json_response, dict) or len(json_response) != 1:
            # Could not validate the message is a single key-value pair
            raise Exception(_("Unexpected response format from your KA Lite installation."))
        message_type = json_response.keys()[0]
        message = json_response.values()[0]
    except ValueError as e:
        message_type = "error"
        message = unicode(e)
    except Exception as e:
        message_type = "error"
        message = _("Loading json object: %s") % e

    # If something broke on the distributed server, we have no way to recover.
    #   For now, just show the error to users.
    #
    # Ultimately, we have a message, would like to share with the distributed server.
#    if response.status_code != 200:
#        return HttpResponseServerError(response.content)

    return HttpResponseRedirect(set_query_params(request.session["distributed_redirect_url"], {
        "message_type": message_type,
        "message": message,
        "message_id": "id_khanload",
    }))
Example #38
0
def compute_data(data_types, who, where, language=settings.LANGUAGE_CODE):
    """
    Compute the data in "data_types" for each user in "who", for the topics selected by "where"

    who: list of users
    where: topic_path
    data_types can include:
        pct_mastery
        effort
        attempts
    """

    # None indicates that the data hasn't been queried yet.
    #   We'll query it on demand, for efficiency
    topics = None
    exercises = None
    videos = None

    # Initialize an empty dictionary of data, video logs, exercise logs, for each user
    data = OrderedDict(
        zip([w.id for w in who],
            [dict()
             for i in range(len(who))]))  # maintain the order of the users
    vid_logs = dict(zip([w.id for w in who], [[] for i in range(len(who))]))
    ex_logs = dict(zip([w.id for w in who], [[] for i in range(len(who))]))
    if UserLog.is_enabled():
        activity_logs = dict(
            zip([w.id for w in who], [[] for i in range(len(who))]))

    # Set up queries (but don't run them), so we have really easy aliases.
    #   Only do them if they haven't been done yet (tell this by passing in a value to the lambda function)
    # Topics: topics.
    # Exercises: names (ids for ExerciseLog objects)
    # Videos: video_id (ids for VideoLog objects)

    # This lambda partial creates a function to return all items with a particular path from the NODE_CACHE.
    search_fun_single_path = partial(lambda t, p: t["path"].startswith(p),
                                     p=tuple(where))
    # This lambda partial creates a function to return all items with paths matching a list of paths from NODE_CACHE.
    search_fun_multi_path = partial(
        lambda ts, p: any([t["path"].startswith(p) for t in ts]),
        p=tuple(where))
    # Functions that use the functions defined above to return topics, exercises, and videos based on paths.
    query_topics = partial(lambda t, sf: t if t is not None else [
        t["id"]
        for t in filter(sf,
                        get_node_cache('Topic', language=language).values())
    ],
                           sf=search_fun_single_path)
    query_exercises = partial(lambda e, sf: e if e is not None else [
        ex["id"]
        for ex in filter(sf,
                         get_exercise_cache(language=language).values())
    ],
                              sf=search_fun_single_path)
    query_videos = partial(lambda v, sf: v if v is not None else [
        vid["id"] for vid in filter(
            sf,
            get_node_cache('Content', language=language).values())
    ],
                           sf=search_fun_single_path)

    # No users, don't bother.
    if len(who) > 0:

        # Query out all exercises, videos, exercise logs, and video logs before looping to limit requests.
        # This means we could pull data for n-dimensional coach report displays with the same number of requests!
        # Note: User activity is polled inside the loop, to prevent possible slowdown for exercise and video reports.
        exercises = query_exercises(exercises)

        videos = query_videos(videos)

        if exercises:
            ex_logs = query_logs(data.keys(), exercises, "exercise", ex_logs)

        if videos:
            vid_logs = query_logs(data.keys(), videos, "video", vid_logs)

        for data_type in (data_types if not hasattr(data_types, "lower") else [
                data_types
        ]):  # convert list from string, if necessary
            if data_type in data[data.keys(
            )[0]]:  # if the first user has it, then all do; no need to calc again.
                continue

            #
            # These are summary stats: you only get one per user
            #
            if data_type == "pct_mastery":

                # Efficient query out, spread out to dict
                for user in data.keys():
                    data[user][
                        data_type] = 0 if not ex_logs[user] else 100. * sum(
                            [el['complete']
                             for el in ex_logs[user]]) / float(len(exercises))

            elif data_type == "effort":
                if "ex:attempts" in data[data.keys(
                )[0]] and "vid:total_seconds_watched" in data[data.keys()[0]]:
                    # exercises and videos would be initialized already
                    for user in data.keys():
                        avg_attempts = 0 if len(exercises) == 0 else sum(
                            data[user]["ex:attempts"].values()) / float(
                                len(exercises))
                        avg_video_points = 0 if len(videos) == 0 else sum(
                            data[user]["vid:total_seconds_watched"].values(
                            )) / float(len(videos))
                        data[user][data_type] = 100. * (
                            0.5 * avg_attempts / 10. +
                            0.5 * avg_video_points / 750.)
                else:
                    data_types += [
                        "ex:attempts", "vid:total_seconds_watched", "effort"
                    ]

            #
            # These are detail stats: you get many per user
            #
            # Just querying out data directly: Video
            elif data_type.startswith("vid:") and data_type[4:] in [
                    f.name for f in VideoLog._meta.fields
            ]:

                for user in data.keys():
                    data[user][data_type] = OrderedDict([
                        (v['video_id'], v[data_type[4:]])
                        for v in vid_logs[user]
                    ])

            # Just querying out data directly: Exercise
            elif data_type.startswith("ex:") and data_type[3:] in [
                    f.name for f in ExerciseLog._meta.fields
            ]:

                for user in data.keys():
                    data[user][data_type] = OrderedDict([
                        (el['exercise_id'], el[data_type[3:]])
                        for el in ex_logs[user]
                    ])

            # User Log Queries
            elif data_type.startswith("user:"******"", "activity",
                                           activity_logs)

                for user in data.keys():
                    data[user][data_type] = [
                        log[data_type[5:]] for log in activity_logs[user]
                    ]

            # User Summary Queries
            elif data_type.startswith("usersum:") and data_type[8:] in [
                    f.name for f in UserLogSummary._meta.fields
            ] and UserLog.is_enabled():

                activity_logs = query_logs(data.keys(), "", "summaryactivity",
                                           activity_logs)

                for user in data.keys():
                    data[user][data_type] = sum(
                        [log[data_type[8:]] for log in activity_logs[user]])
            # Unknown requested quantity
            else:
                raise Exception(
                    "Unknown type: '%s' not in %s" %
                    (data_type, str([f.name
                                     for f in ExerciseLog._meta.fields])))

    # Returning empty list instead of None allows javascript on client
    # side to read 'length' property without error.
    exercises = exercises or []

    videos = videos or []

    return {
        "data": data,
        "topics": topics,
        "exercises": exercises,
        "videos": videos,
    }
Example #39
0
def api_data(request, xaxis="", yaxis=""):
    """Request contains information about what data are requested (who, what, and how).

    Response should be a JSON object
    * data contains the data, structred by user and then datatype
    * the rest of the data is metadata, useful for displaying detailed info about data.
    """

    language = lcode_to_django_lang(request.language)
    # Get the request form
    try:
        form = get_data_form(request, xaxis=xaxis,
                             yaxis=yaxis)  # (data=request.REQUEST)
    except Exception as e:
        # In investigating #1509: we can catch SQL errors here and communicate clearer error
        #   messages with the user here.  For now, we have no such error to catch, so just
        #   pass the errors on to the user (via the @api_handle_error_with_json decorator).
        raise e

    # Query out the data: who?
    if form.data.get("user"):
        facility = []
        groups = []
        users = [get_object_or_404(FacilityUser, id=form.data.get("user"))]
    elif form.data.get("group"):
        facility = []
        if form.data.get("group") == "Ungrouped":
            groups = []
            users = FacilityUser.objects.filter(
                facility__in=[form.data.get("facility")],
                group__isnull=True,
                is_teacher=False).order_by("last_name", "first_name")
        else:
            groups = [
                get_object_or_404(FacilityGroup, id=form.data.get("group"))
            ]
            users = FacilityUser.objects.filter(group=form.data.get("group"),
                                                is_teacher=False).order_by(
                                                    "last_name", "first_name")
    elif form.data.get("facility"):
        facility = get_object_or_404(Facility, id=form.data.get("facility"))
        groups = FacilityGroup.objects.filter(
            facility__in=[form.data.get("facility")])
        users = FacilityUser.objects.filter(
            facility__in=[form.data.get("facility")],
            is_teacher=False).order_by("last_name", "first_name")
    else:
        # Allow superuser to see the data.
        if request.user.is_authenticated() and request.user.is_superuser:
            facility = []
            groups = []
            users = FacilityUser.objects.all().order_by(
                "last_name", "first_name")
        else:
            return HttpResponseNotFound(
                _("Did not specify facility, group, nor user."))

    # Query out the data: where?
    if not form.data.get("topic_path"):
        return HttpResponseNotFound(_("Must specify a topic path"))

    # Query out the data: what?
    computed_data = compute_data(
        data_types=[form.data.get("xaxis"),
                    form.data.get("yaxis")],
        who=users,
        where=form.data.get("topic_path"),
        language=language)

    # Quickly add back in exercise meta-data (could potentially be used in future for other data too!)
    ex_nodes = get_node_cache(language=language)["Exercise"]
    exercises = []
    for e in computed_data["exercises"]:
        exercises.append({
            "slug": e,
            "full_name": ex_nodes[e]["display_name"],
            "url": ex_nodes[e]["path"],
        })

    json_data = {
        "data":
        computed_data["data"],
        "exercises":
        exercises,
        "videos":
        computed_data["videos"],
        "users":
        dict(
            zip([u.id for u in users], [
                "%s, %s" % (u.last_name, u.first_name)
                if u.last_name or u.first_name else u.username for u in users
            ])),
        "groups":
        dict(
            zip(
                [g.id for g in groups],
                dict(zip(["id", "name"], [(g.id, g.name) for g in groups])),
            )),
        "facility":
        None if not facility else {
            "name": facility.name,
            "id": facility.id,
        }
    }

    if "facility_user" in request.session:
        try:
            # Log a "begin" and end here
            user = request.session["facility_user"]
            UserLog.begin_user_activity(user, activity_type="coachreport")
            UserLog.update_user_activity(
                user, activity_type="login"
            )  # to track active login time for teachers
            UserLog.end_user_activity(user, activity_type="coachreport")
        except ValidationError as e:
            # Never report this error; don't want this logging to block other functionality.
            logging.error(
                "Failed to update Teacher userlog activity login: %s" % e)

    # Now we have data, stream it back with a handler for date-times
    return JsonResponse(json_data)
Example #40
0
def tabular_view(request, report_type="exercise"):
    """Tabular view also gets data server-side."""
    # important for setting the defaults for the coach nav bar

    language = lcode_to_django_lang(request.language)

    facility, group_id, context = coach_nav_context(request, "tabular")

    # Define how students are ordered--used to be as efficient as possible.
    student_ordering = ["last_name", "first_name", "username"]

    # Get a list of topics (sorted) and groups
    topics = [
        get_node_cache("Topic", language=language).get(tid["id"])
        for tid in get_knowledgemap_topics(language=language)
        if report_type.title() in tid["contains"]
    ]
    playlists = Playlist.all()

    (groups, facilities,
     ungrouped_available) = get_accessible_objects_from_logged_in_user(
         request, facility=facility)

    context.update(plotting_metadata_context(request, facility=facility))

    context.update({
        # For translators: the following two translations are nouns
        "report_types": ({
            "value": "exercise",
            "name": _("exercise")
        }, {
            "value": "video",
            "name": _("video")
        }),
        "request_report_type":
        report_type,
        "topics": [{
            "id": t["id"],
            "title": t["title"]
        } for t in topics if t],
        "playlists": [{
            "id": p.id,
            "title": p.title,
            "tag": p.tag
        } for p in playlists if p],
    })

    # get querystring info
    topic_id = request.GET.get("topic", "")
    playlist_id = request.GET.get("playlist", "")
    # No valid data; just show generic
    # Exactly one of topic_id or playlist_id should be present
    if not ((topic_id or playlist_id) and not (topic_id and playlist_id)):
        if playlists:
            messages.add_message(request, WARNING,
                                 _("Please select a playlist."))
        elif topics:
            messages.add_message(request, WARNING, _("Please select a topic."))
        return context

    playlist = (filter(lambda p: p.id == playlist_id, Playlist.all())
                or [None])[0]

    if group_id:
        # Narrow by group
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(group__isnull=True,
                                                is_teacher=False)
            if facility:
                # filter only those ungrouped students for the facility
                users = users.filter(facility=facility)
                users = users.order_by(*student_ordering)
            else:
                # filter all ungroup students
                users = FacilityUser.objects.filter(
                    group__isnull=True,
                    is_teacher=False).order_by(*student_ordering)

        else:
            users = FacilityUser.objects.filter(
                group=group_id, is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [
            groups_dict["groups"] for groups_dict in groups
            if groups_dict["facility"] == facility.id
        ]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[
            0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility),
            is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None),
            is_teacher=False).order_by(*student_ordering)

    # We have enough data to render over a group of students
    # Get type-specific information
    if report_type == "exercise":
        # Fill in exercises
        if topic_id:
            exercises = get_topic_exercises(topic_id=topic_id)
        elif playlist:
            exercises = playlist.get_playlist_entries("Exercise",
                                                      language=language)

        context["exercises"] = exercises

        # More code, but much faster
        exercise_names = [ex["id"] for ex in context["exercises"]]
        # Get students
        context["students"] = []
        exlogs = ExerciseLog.objects \
            .filter(user__in=users, exercise_id__in=exercise_names) \
            .order_by(*["user__%s" % field for field in student_ordering]) \
            .values("user__id", "struggling", "complete", "exercise_id")
        exlogs = list(exlogs)  # force the query to be evaluated

        exlog_idx = 0
        for user in users:
            log_table = {}
            while exlog_idx < len(
                    exlogs) and exlogs[exlog_idx]["user__id"] == user.id:
                log_table[exlogs[exlog_idx]["exercise_id"]] = exlogs[exlog_idx]
                exlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "exercise_logs": log_table,
            })

    elif report_type == "video":
        # Fill in videos
        if topic_id:
            context["videos"] = get_topic_videos(topic_id=topic_id)
        elif playlist:
            context["videos"] = playlist.get_playlist_entries(
                "Video", language=language)

        # More code, but much faster
        video_ids = [vid["id"] for vid in context["videos"]]
        # Get students
        context["students"] = []
        vidlogs = VideoLog.objects \
            .filter(user__in=users, video_id__in=video_ids) \
            .order_by(*["user__%s" % field for field in student_ordering])\
            .values("user__id", "complete", "video_id", "total_seconds_watched", "points")
        vidlogs = list(vidlogs)  # force the query to be executed now

        vidlog_idx = 0
        for user in users:
            log_table = {}
            while vidlog_idx < len(
                    vidlogs) and vidlogs[vidlog_idx]["user__id"] == user.id:
                log_table[vidlogs[vidlog_idx]
                          ["video_id"]] = vidlogs[vidlog_idx]
                vidlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "video_logs": log_table,
            })
    else:
        raise Http404(
            _("Unknown report_type: %(report_type)s") %
            {"report_type": report_type})

    # Validate results by showing user messages.
    if not users:
        # 1. check group facility groups
        if len(groups) > 0 and not groups[0]['groups']:
            # 1. No groups available (for facility) and "no students" returned.
            messages.add_message(
                request, WARNING,
                _("No learner accounts have been created for selected facility/group."
                  ))
        elif topic_id and playlist_id:
            # 2. Both topic and playlist are selected.
            messages.add_message(
                request, WARNING,
                _("Please select either a topic or a playlist above, but not both."
                  ))
        elif not topic_id and not playlist_id:
            # 3. Group was selected, but data not queried because a topic or playlist was not selected.
            if playlists:
                # 4. No playlist was selected.
                messages.add_message(request, WARNING,
                                     _("Please select a playlist."))
            elif topics:
                # 5. No topic was selected.
                messages.add_message(request, WARNING,
                                     _("Please select a topic."))
        else:
            # 6. Everything specified, but no users fit the query.
            messages.add_message(
                request, WARNING,
                _("No learner accounts in this group have been created."))
    # End: Validate results by showing user messages.

    log_coach_report_view(request)

    return context
Example #41
0
def tabular_view(request, facility, report_type="exercise"):
    """Tabular view also gets data server-side."""
    # Define how students are ordered--used to be as efficient as possible.
    student_ordering = ["last_name", "first_name", "username"]

    # Get a list of topics (sorted) and groups
    topics = [get_node_cache("Topic").get(tid) for tid in get_knowledgemap_topics()]
    (groups, facilities) = get_accessible_objects_from_logged_in_user(request, facility=facility)
    context = plotting_metadata_context(request, facility=facility)
    context.update({
        # For translators: the following two translations are nouns
        "report_types": (_("exercise"), _("video")),
        "request_report_type": report_type,
        "topics": [{"id": t[0]["id"], "title": t[0]["title"]} for t in topics if t],
    })

    # get querystring info
    topic_id = request.GET.get("topic", "")
    # No valid data; just show generic
    if not topic_id or not re.match("^[\w\-]+$", topic_id):
        return context

    group_id = request.GET.get("group", "")
    if group_id:
        # Narrow by group
        users = FacilityUser.objects.filter(
            group=group_id, is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [groups_dict["groups"] for groups_dict in groups if groups_dict["facility"] == facility.id]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility), is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None), is_teacher=False).order_by(*student_ordering)

    # We have enough data to render over a group of students
    # Get type-specific information
    if report_type == "exercise":
        # Fill in exercises
        exercises = get_topic_exercises(topic_id=topic_id)
        exercises = sorted(exercises, key=lambda e: (e["h_position"], e["v_position"]))
        context["exercises"] = exercises

        # More code, but much faster
        exercise_names = [ex["name"] for ex in context["exercises"]]
        # Get students
        context["students"] = []
        exlogs = ExerciseLog.objects \
            .filter(user__in=users, exercise_id__in=exercise_names) \
            .order_by(*["user__%s" % field for field in student_ordering]) \
            .values("user__id", "struggling", "complete", "exercise_id")
        exlogs = list(exlogs)  # force the query to be evaluated

        exlog_idx = 0
        for user in users:
            log_table = {}
            while exlog_idx < len(exlogs) and exlogs[exlog_idx]["user__id"] == user.id:
                log_table[exlogs[exlog_idx]["exercise_id"]] = exlogs[exlog_idx]
                exlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "exercise_logs": log_table,
            })

    elif report_type == "video":
        # Fill in videos
        context["videos"] = get_topic_videos(topic_id=topic_id)

        # More code, but much faster
        video_ids = [vid["id"] for vid in context["videos"]]
        # Get students
        context["students"] = []
        vidlogs = VideoLog.objects \
            .filter(user__in=users, video_id__in=video_ids) \
            .order_by(*["user__%s" % field for field in student_ordering])\
            .values("user__id", "complete", "video_id", "total_seconds_watched", "points")
        vidlogs = list(vidlogs)  # force the query to be executed now

        vidlog_idx = 0
        for user in users:
            log_table = {}
            while vidlog_idx < len(vidlogs) and vidlogs[vidlog_idx]["user__id"] == user.id:
                log_table[vidlogs[vidlog_idx]["video_id"]] = vidlogs[vidlog_idx]
                vidlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "video_logs": log_table,
            })

    else:
        raise Http404(_("Unknown report_type: %(report_type)s") % {"report_type": report_type})

    if "facility_user" in request.session:
        try:
            # Log a "begin" and end here
            user = request.session["facility_user"]
            UserLog.begin_user_activity(user, activity_type="coachreport")
            UserLog.update_user_activity(user, activity_type="login")  # to track active login time for teachers
            UserLog.end_user_activity(user, activity_type="coachreport")
        except ValidationError as e:
            # Never report this error; don't want this logging to block other functionality.
            logging.error("Failed to update Teacher userlog activity login: %s" % e)

    return context
Example #42
0
def student_view_context(request, xaxis="pct_mastery", yaxis="ex:attempts"):
    """
    Context done separately, to be importable for similar pages.
    """
    user = get_user_from_request(request=request)
    if not user:
        raise Http404("User not found.")

    node_cache = get_node_cache()
    topic_ids = get_knowledgemap_topics()
    topic_ids = topic_ids + [ch["id"] for node in get_topic_tree()["children"] for ch in node["children"] if node["id"] != "math"]
    topics = [node_cache["Topic"][id][0] for id in topic_ids]

    user_id = user.id
    exercise_logs = list(ExerciseLog.objects \
        .filter(user=user) \
        .values("exercise_id", "complete", "points", "attempts", "streak_progress", "struggling", "completion_timestamp"))
    video_logs = list(VideoLog.objects \
        .filter(user=user) \
        .values("video_id", "complete", "total_seconds_watched", "points", "completion_timestamp"))

    exercise_sparklines = dict()
    stats = dict()
    topic_exercises = dict()
    topic_videos = dict()
    exercises_by_topic = dict()
    videos_by_topic = dict()

    # Categorize every exercise log into a "midlevel" exercise
    for elog in exercise_logs:
        if not elog["exercise_id"] in node_cache["Exercise"]:
            # Sometimes KA updates their topic tree and eliminates exercises;
            #   we also want to support 3rd party switching of trees arbitrarily.
            logging.debug("Skip unknown exercise log for %s/%s" % (user_id, elog["exercise_id"]))
            continue

        parent_ids = [topic for ex in node_cache["Exercise"][elog["exercise_id"]] for topic in ex["ancestor_ids"]]
        topic = set(parent_ids).intersection(set(topic_ids))
        if not topic:
            logging.error("Could not find a topic for exercise %s (parents=%s)" % (elog["exercise_id"], parent_ids))
            continue
        topic = topic.pop()
        if not topic in topic_exercises:
            topic_exercises[topic] = get_topic_exercises(path=node_cache["Topic"][topic][0]["path"])
        exercises_by_topic[topic] = exercises_by_topic.get(topic, []) + [elog]

    # Categorize every video log into a "midlevel" exercise.
    for vlog in video_logs:
        if not vlog["video_id"] in node_cache["Video"]:
            # Sometimes KA updates their topic tree and eliminates videos;
            #   we also want to support 3rd party switching of trees arbitrarily.
            logging.debug("Skip unknown video log for %s/%s" % (user_id, vlog["video_id"]))
            continue

        parent_ids = [topic for vid in node_cache["Video"][vlog["video_id"]] for topic in vid["ancestor_ids"]]
        topic = set(parent_ids).intersection(set(topic_ids))
        if not topic:
            logging.error("Could not find a topic for video %s (parents=%s)" % (vlog["video_id"], parent_ids))
            continue
        topic = topic.pop()
        if not topic in topic_videos:
            topic_videos[topic] = get_topic_videos(path=node_cache["Topic"][topic][0]["path"])
        videos_by_topic[topic] = videos_by_topic.get(topic, []) + [vlog]


    # Now compute stats
    for id in topic_ids:#set(topic_exercises.keys()).union(set(topic_videos.keys())):
        n_exercises = len(topic_exercises.get(id, []))
        n_videos = len(topic_videos.get(id, []))

        exercises = exercises_by_topic.get(id, [])
        videos = videos_by_topic.get(id, [])
        n_exercises_touched = len(exercises)
        n_videos_touched = len(videos)

        exercise_sparklines[id] = [el["completion_timestamp"] for el in filter(lambda n: n["complete"], exercises)]

        # total streak currently a pct, but expressed in max 100; convert to
        # proportion (like other percentages here)
        stats[id] = {
            "ex:pct_mastery":      0 if not n_exercises_touched else sum([el["complete"] for el in exercises]) / float(n_exercises),
            "ex:pct_started":      0 if not n_exercises_touched else n_exercises_touched / float(n_exercises),
            "ex:average_points":   0 if not n_exercises_touched else sum([el["points"] for el in exercises]) / float(n_exercises_touched),
            "ex:average_attempts": 0 if not n_exercises_touched else sum([el["attempts"] for el in exercises]) / float(n_exercises_touched),
            "ex:average_streak":   0 if not n_exercises_touched else sum([el["streak_progress"] for el in exercises]) / float(n_exercises_touched) / 100.,
            "ex:total_struggling": 0 if not n_exercises_touched else sum([el["struggling"] for el in exercises]),
            "ex:last_completed": None if not n_exercises_touched else max_none([el["completion_timestamp"] or None for el in exercises]),

            "vid:pct_started":      0 if not n_videos_touched else n_videos_touched / float(n_videos),
            "vid:pct_completed":    0 if not n_videos_touched else sum([vl["complete"] for vl in videos]) / float(n_videos),
            "vid:total_minutes":      0 if not n_videos_touched else sum([vl["total_seconds_watched"] for vl in videos]) / 60.,
            "vid:average_points":   0. if not n_videos_touched else float(sum([vl["points"] for vl in videos]) / float(n_videos_touched)),
            "vid:last_completed": None if not n_videos_touched else max_none([vl["completion_timestamp"] or None for vl in videos]),
        }

    context = plotting_metadata_context(request)

    return {
        "form": context["form"],
        "groups": context["groups"],
        "facilities": context["facilities"],
        "student": user,
        "topics": topics,
        "exercises": topic_exercises,
        "exercise_logs": exercises_by_topic,
        "video_logs": videos_by_topic,
        "exercise_sparklines": exercise_sparklines,
        "no_data": not exercise_logs and not video_logs,
        "stats": stats,
        "stat_defs": [  # this order determines the order of display
            {"key": "ex:pct_mastery",      "title": _("% Mastery"),        "type": "pct"},
            {"key": "ex:pct_started",      "title": _("% Started"),        "type": "pct"},
            {"key": "ex:average_points",   "title": _("Average Points"),   "type": "float"},
            {"key": "ex:average_attempts", "title": _("Average Attempts"), "type": "float"},
            {"key": "ex:average_streak",   "title": _("Average Streak"),   "type": "pct"},
            {"key": "ex:total_struggling", "title": _("Struggling"),       "type": "int"},
            {"key": "ex:last_completed",   "title": _("Last Completed"),   "type": "date"},
            {"key": "vid:pct_completed",   "title": _("% Completed"),      "type": "pct"},
            {"key": "vid:pct_started",     "title": _("% Started"),        "type": "pct"},
            {"key": "vid:total_minutes",   "title": _("Average Minutes Watched"),"type": "float"},
            {"key": "vid:average_points",  "title": _("Average Points"),   "type": "float"},
            {"key": "vid:last_completed",  "title": _("Last Completed"),   "type": "date"},
        ]
    }
 def test_topic_availability(self):
     for topic in get_node_cache("Topic").values():
         any_available = bool(sum([v.get("available", False) for v in topic["children"]]))
         self.assertEqual(topic["available"], any_available, "Make sure topic availability matches video availability when only videos are available.")
Example #44
0
def generate_dubbed_video_mappings_from_csv(csv_data=None):

    # This CSV file is in standard format: separated by ",", quoted by '"'
    logging.info("Parsing csv file.")
    reader = csv.reader(StringIO(csv_data))

    # Build a two-level video map.
    #   First key: language name
    #   Second key: english youtube ID
    #   Value: corresponding youtube ID in the new language.
    video_map = {}

    # Loop through each row in the spreadsheet.
    for row in reader:

        # skip over the header rows
        if row[0].strip() in ["", "UPDATED:"]:
            continue

        elif row[0] == "SERIAL":
            # Read the header row.
            header_row = [
                v.lower() for v in row
            ]  # lcase all header row values (including language names)
            slug_idx = header_row.index("title id")
            english_idx = header_row.index("english")
            assert slug_idx != -1, "Video slug column header should be found."
            assert english_idx != -1, "English video column header should be found."

        else:
            # Rows 6 and beyond are data.
            assert len(row) == len(
                header_row), "Values line length equals headers line length"

            # Grab the slug and english video ID.
            video_slug = row[slug_idx]
            english_video_id = row[english_idx]
            assert english_video_id, "English Video ID should not be empty"
            assert video_slug, "Slug should not be empty"

            # English video is the first video ID column,
            #   and following columns (until the end) are other languages.
            # Loop through those columns and, if a video exists,
            #   add it to the dictionary.
            for idx in range(english_idx, len(row)):
                if not row[idx]:  # make sure there's a dubbed video
                    continue

                lang = header_row[idx]
                if lang not in video_map:  # add the first level if it doesn't exist
                    video_map[lang] = {}
                dubbed_youtube_id = row[idx]
                if english_video_id == dubbed_youtube_id and lang != "english":
                    logging.error(
                        "Removing entry for (%s, %s): dubbed and english youtube ID are the same."
                        % (lang, english_video_id))
                #elif dubbed_youtube_id in video_map[lang].values():
                # Talked to Bilal, and this is actually supposed to be OK.  Would throw us for a loop!
                #    For now, just keep one.
                #for key in video_map[lang].keys():
                #    if video_map[lang][key] == dubbed_youtube_id:
                #        del video_map[lang][key]
                #        break
                #logging.error("Removing entry for (%s, %s): the same dubbed video ID is used in two places, and we can only keep one in our current system." % (lang, english_video_id))
                else:
                    video_map[lang][english_video_id] = row[
                        idx]  # add the corresponding video id for the video, in this language.

    # Now, validate the mappings with our topic data
    known_videos = get_node_cache("Video").keys()
    missing_videos = set(known_videos) - set(video_map["english"].keys())
    extra_videos = set(video_map["english"].keys()) - set(known_videos)
    if missing_videos:
        logging.warn(
            "There are %d known videos not in the list of dubbed videos" %
            len(missing_videos))
        logging.warn(
            "Adding missing English videos to English dubbed video map")
        for video in missing_videos:
            video_map["english"][video] = video
    if extra_videos:
        logging.warn(
            "There are %d videos in the list of dubbed videos that we have never heard of."
            % len(extra_videos))

    return video_map
Example #45
0
 def clean_exercise_id(self):
     """
     Make sure the exercise ID is found.
     """
     if not self.cleaned_data.get("exercise_id", "") in get_node_cache('Exercise'):
         raise forms.ValidationError(_("Exercise ID not recognized"))
Example #46
0
 def test_topic_availability(self):
     for topic in get_node_cache("Topic").values():
         if topic.get("kind") == "Topic":
             any_available = bool(sum([get_node_cache("Topic").get(v, {}).get("available", False) for v in topic.get("children", [])]))
             self.assertEqual(topic["available"], any_available, "Make sure topic availability matches child availability when any children are available.")
Example #47
0
def search(request):
    # Inputs
    page = int(request.GET.get('page', 1))
    query = request.GET.get('query')
    category = request.GET.get('category')
    max_results_per_category = request.GET.get('max_results', 25)

    # Outputs
    query_error = None
    possible_matches = {}
    hit_max = {}


    node_kinds = {
        "Topic": ["Topic"],
        "Exercise": ["Exercise"],
        "Content": ["Video", "Audio", "Document"],
    }

    if query is None:
        query_error = _("Error: query not specified.")

#    elif len(query) < 3:
#        query_error = _("Error: query too short.")

    else:
        query = query.lower()
        # search for topic, video or exercise with matching title
        for node_type, node_dict in topic_tools.get_node_cache().iteritems():
            if category and node_type != category:
                # Skip categories that don't match (if specified)
                continue

            exact_match = filter(lambda node: node["kind"] in node_kinds[node_type] and node["title"].lower() == query, node_dict.values())[:1]

            if exact_match:
                # Redirect to an exact match
                return HttpResponseRedirect(reverse('learn') + exact_match[0]['path'])

            # For efficiency, don't do substring matches when we've got lots of results
            match_generator = (node for node in node_dict.values()
                if node["kind"] in node_kinds[node_type] and (query in node["title"].lower() or query in [x.lower() for x in node.get('keywords', [])] or
                query in [x.lower() for x in node.get('tags', [])]))

            # Only return max results
            try:
                possible_matches[node_type] = list(islice(match_generator, (page-1)*max_results_per_category, page*max_results_per_category))
            except ValueError:
                return HttpResponseNotFound("Page does not exist")

            hit_max[node_type] = next(match_generator, False)

    previous_params = request.GET.copy()
    previous_params['page'] = page - 1

    previous_url = "?" + previous_params.urlencode()

    next_params = request.GET.copy()
    next_params['page'] = page + 1

    next_url = "?" + next_params.urlencode()

    return {
        'title': _("Search results for '%(query)s'") % {"query": (query if query else "")},
        'query_error': query_error,
        'results': possible_matches,
        'hit_max': hit_max,
        'more': any(hit_max.values()),
        'page': page,
        'previous_url': previous_url,
        'next_url': next_url,
        'query': query,
        'max_results': max_results_per_category,
        'category': category,
    }
Example #48
0
class StudentExerciseTest(BrowserActionMixins, FacilityMixins, KALiteBrowserTestCase):
    """
    Test exercises.
    """
    student_username = '******'
    student_password =  '******'
    EXERCISE_SLUG = 'addition_1'
    MIN_POINTS = get_node_cache("Exercise")[EXERCISE_SLUG]["basepoints"]
    MAX_POINTS = 2 * MIN_POINTS

    def setUp(self):
        """
        Create a student, log the student in, and go to the exercise page.
        """
        super(StudentExerciseTest, self).setUp()
        self.facility_name = "fac"
        self.facility = self.create_facility(name=self.facility_name)
        self.student = self.create_student(username=self.student_username,
                                           password=self.student_password,
                                           facility=self.facility)
        self.browser_login_student(self.student_username, self.student_password, facility_name=self.facility_name)

        self.browse_to(self.live_server_url + reverse("learn") + get_node_cache("Exercise")[self.EXERCISE_SLUG]["path"])
        self.nanswers = self.browser.execute_script('return window.ExerciseParams.STREAK_CORRECT_NEEDED;')

    def browser_get_current_points(self):
        """
        Check the total points a student has accumulated, from an exercise page.
        """
        try:
            points_regexp = r'\((?P<points>\w+) points\)'
            points_text = self.browser.find_element_by_css_selector('.progress-points').text
            points = re.match(points_regexp, points_text).group('points')
            return points
        except AttributeError:
            return ""

    def browser_submit_answer(self, answer):
        """
        From an exercise page, insert an answer into the text box and submit.
        """
        ui.WebDriverWait(self.browser, 10).until(
            expected_conditions.presence_of_element_located((By.ID, 'solutionarea'))
        )
        self.browser.find_element_by_id('solutionarea').find_element_by_css_selector('input[type=text]').click()
        self.browser_send_keys(unicode(answer))
        self.browser.find_element_by_id('check-answer-button').click()

        try:
            ui.WebDriverWait(self.browser, 10).until(
                expected_conditions.visibility_of_element_located((By.ID, 'next-question-button'))
            )
            correct = self.browser.find_element_by_id('next-question-button').get_attribute("value")=="Correct! Next question..."
        except TimeoutException:
            correct = False
        return correct

    @unittest.skipIf(settings.RUNNING_IN_TRAVIS, "I CAN'T TAKE THIS ANYMORE!")
    @unittest.skipIf(getattr(settings, 'CONFIG_PACKAGE', None), "Fails if settings.CONFIG_PACKAGE is set.")
    def test_question_correct_points_are_added(self):
        """
        Answer an exercise correctly
        """
        ui.WebDriverWait(self.browser, 10).until(
            expected_conditions.presence_of_element_located((By.CLASS_NAME, 'mord'))
        )
        numbers = self.browser.find_elements_by_css_selector("span[class=mord][style]")
        answer = sum(int(num.text) for num in numbers)
        correct = self.browser_submit_answer(answer)
        self.assertTrue(correct, "answer was incorrect")
        elog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_SLUG, user=self.student)
        self.assertEqual(elog.streak_progress, 100 / self.nanswers, "Streak progress should be 10%")
        self.assertFalse(elog.struggling, "Student is not struggling.")
        self.assertEqual(elog.attempts, 1, "Student should have 1 attempt.")
        self.assertFalse(elog.complete, "Student should not have completed the exercise.")
        self.assertEqual(elog.attempts_before_completion, None, "Student should not have a value for attempts_before_completion.")

    @unittest.skipIf(settings.RUNNING_IN_TRAVIS, "I CAN'T TAKE THIS ANYMORE!")
    def test_question_incorrect_false(self):
        """
        Answer an exercise incorrectly.
        """
        ui.WebDriverWait(self.browser, 10).until(
            expected_conditions.presence_of_element_located((By.CLASS_NAME, 'mord'))
        )
        correct = self.browser_submit_answer(0)
        self.assertFalse(correct, "answer was correct")

        elog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_SLUG, user=self.student)
        self.assertEqual(elog.streak_progress, 0, "Streak progress should be 0%")
        self.assertFalse(elog.struggling, "Student is not struggling.")
        self.assertEqual(elog.attempts, 1, "Student should have 1 attempt.")
        self.assertFalse(elog.complete, "Student should not have completed the exercise.")
        self.assertEqual(elog.attempts_before_completion, None, "Student should not have a value for attempts_before_completion.")

    @unittest.skipIf(settings.RUNNING_IN_TRAVIS, "I CAN'T TAKE THIS ANYMORE!")
    def test_question_incorrect_button_text_changes(self):
        """
        Answer an exercise incorrectly, and make sure button text changes.
        """
        ui.WebDriverWait(self.browser, 10).until(
            expected_conditions.presence_of_element_located((By.CLASS_NAME, 'mord'))
        )

        self.browser_submit_answer(0)

        answer_button_text = self.browser.find_element_by_id("check-answer-button").get_attribute("value")

        self.assertTrue(answer_button_text == "Check Answer", "Answer button changed on incorrect answer!")

    # @unittest.skipIf(getattr(settings, 'CONFIG_PACKAGE', None), "Fails if settings.CONFIG_PACKAGE is set.")
    @unittest.skipIf(settings.RUNNING_IN_TRAVIS, "I CAN'T TAKE THIS ANYMORE!")
    @override_settings(CONFIG_PACKAGE=None)
    def test_exercise_mastery(self):
        """
        Answer an exercise til mastery
        """
        for ai in range(1, 1 + self.nanswers):
            # Hey future maintainer! The visibility_of_element_located
            # requires that the element be ACTUALLY visible on the screen!
            # so you can't just have the test spawn a teeny-bitty browser to
            # the side while you have the world cup occupying a big part of your
            # screen.
            ui.WebDriverWait(self.browser, 10).until(
                expected_conditions.visibility_of_element_located((By.CLASS_NAME, 'mord'))
            )
            numbers = self.browser.find_elements_by_css_selector("span[class=mord][style]")
            answer = sum(int(num.text) for num in numbers)
            correct = self.browser_submit_answer(answer)
            self.assertTrue(correct, "answer was incorrect")

            self.browser_send_keys(Keys.RETURN)  # move on to next question.

        # Now test the models
        elog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_SLUG, user=self.student)
        self.assertEqual(elog.streak_progress, 100, "Streak progress should be 100%")
        self.assertFalse(elog.struggling, "Student is not struggling.")
        self.assertEqual(elog.attempts, self.nanswers, "Student should have %s attempts. Got %s" % (self.nanswers, elog.attempts))
        self.assertTrue(elog.complete, "Student should have completed the exercise.")
        self.assertEqual(elog.attempts_before_completion, self.nanswers, "Student should have %s attempts for completion." % self.nanswers)
Example #49
0
def search(request):
    # Inputs
    page = int(request.GET.get('page', 1))
    query = request.GET.get('query')
    category = request.GET.get('category')
    max_results_per_category = request.GET.get('max_results', 25)

    # Outputs
    query_error = None
    possible_matches = {}
    hit_max = {}


    node_kinds = {
        "Topic": ["Topic"],
        "Exercise": ["Exercise"],
        "Content": ["Video", "Audio", "Document"],
    }

    if query is None:
        query_error = _("Error: query not specified.")

#    elif len(query) < 3:
#        query_error = _("Error: query too short.")

    else:
        query = query.lower()
        # search for topic, video or exercise with matching title
        for node_type, node_dict in topic_tools.get_node_cache().iteritems():
            if category and node_type != category:
                # Skip categories that don't match (if specified)
                continue

            exact_match = filter(lambda node: node["kind"] in node_kinds[node_type] and node["title"].lower() == query, node_dict.values())[:1]

            if exact_match:
                # Redirect to an exact match
                return HttpResponseRedirect(reverse('learn') + exact_match[0]['path'])

            # For efficiency, don't do substring matches when we've got lots of results
            match_generator = (node for node in node_dict.values()
                if node["kind"] in node_kinds[node_type] and (query in node["title"].lower() or query in [x.lower() for x in node.get('keywords', [])] or
                query in [x.lower() for x in node.get('tags', [])]))

            # Only return max results
            try:
                possible_matches[node_type] = list(islice(match_generator, (page-1)*max_results_per_category, page*max_results_per_category))
            except ValueError:
                return HttpResponseNotFound("Page does not exist")

            hit_max[node_type] = next(match_generator, False)

    previous_params = request.GET.copy()
    previous_params['page'] = page - 1

    previous_url = "?" + previous_params.urlencode()

    next_params = request.GET.copy()
    next_params['page'] = page + 1

    next_url = "?" + next_params.urlencode()

    return {
        'title': _("Search results for '%(query)s'") % {"query": (query if query else "")},
        'query_error': query_error,
        'results': possible_matches,
        'hit_max': hit_max,
        'more': any(hit_max.values()),
        'page': page,
        'previous_url': previous_url,
        'next_url': next_url,
        'query': query,
        'max_results': max_results_per_category,
        'category': category,
    }
Example #50
0
def _get_user_usage_data(users, groups=None, period_start=None, period_end=None, group_id=None):
    """
    Returns facility user data, within the given date range.
    """

    groups = groups or set([user.group for user in users])

    # compute period start and end
    # Now compute stats, based on queried data
    num_exercises = len(get_node_cache('Exercise'))
    user_data = OrderedDict()
    group_data = OrderedDict()


    # Make queries efficiently
    exercise_logs = ExerciseLog.objects.filter(user__in=users, complete=True)
    video_logs = VideoLog.objects.filter(user__in=users)
    login_logs = UserLogSummary.objects.filter(user__in=users)

    # filter results
    if period_start:
        exercise_logs = exercise_logs.filter(completion_timestamp__gte=period_start)
        video_logs = video_logs.filter(completion_timestamp__gte=period_start)
        login_logs = login_logs.filter(start_datetime__gte=period_start)
    if period_end:
        exercise_logs = exercise_logs.filter(completion_timestamp__lte=period_end)
        video_logs = video_logs.filter(completion_timestamp__lte=period_end)
        login_logs = login_logs.filter(end_datetime__lte=period_end)


    # Force results in a single query
    exercise_logs = list(exercise_logs.values("exercise_id", "user__pk"))
    video_logs = list(video_logs.values("video_id", "user__pk"))
    login_logs = list(login_logs.values("activity_type", "total_seconds", "user__pk"))

    for user in users:
        user_data[user.pk] = OrderedDict()
        user_data[user.pk]["id"] = user.pk
        user_data[user.pk]["first_name"] = user.first_name
        user_data[user.pk]["last_name"] = user.last_name
        user_data[user.pk]["username"] = user.username
        user_data[user.pk]["group"] = user.group


        user_data[user.pk]["total_report_views"] = 0#report_stats["count__sum"] or 0
        user_data[user.pk]["total_logins"] =0# login_stats["count__sum"] or 0
        user_data[user.pk]["total_hours"] = 0#login_stats["total_seconds__sum"] or 0)/3600.

        user_data[user.pk]["total_exercises"] = 0
        user_data[user.pk]["pct_mastery"] = 0.
        user_data[user.pk]["exercises_mastered"] = []

        user_data[user.pk]["total_videos"] = 0
        user_data[user.pk]["videos_watched"] = []


    for elog in exercise_logs:
        user_data[elog["user__pk"]]["total_exercises"] += 1
        user_data[elog["user__pk"]]["pct_mastery"] += 1. / num_exercises
        user_data[elog["user__pk"]]["exercises_mastered"].append(elog["exercise_id"])

    for vlog in video_logs:
        user_data[vlog["user__pk"]]["total_videos"] += 1
        user_data[vlog["user__pk"]]["videos_watched"].append(vlog["video_id"])

    for llog in login_logs:
        if llog["activity_type"] == UserLog.get_activity_int("coachreport"):
            user_data[llog["user__pk"]]["total_report_views"] += 1
        elif llog["activity_type"] == UserLog.get_activity_int("login"):
            user_data[llog["user__pk"]]["total_hours"] += (llog["total_seconds"]) / 3600.
            user_data[llog["user__pk"]]["total_logins"] += 1

    for group in list(groups) + [None]*(group_id==None or group_id=="Ungrouped"):  # None for ungrouped, if no group_id passed.
        group_pk = getattr(group, "pk", None)
        group_name = getattr(group, "name", _("Ungrouped"))
        group_data[group_pk] = {
            "id": group_pk,
            "name": group_name,
            "total_logins": 0,
            "total_hours": 0,
            "total_users": 0,
            "total_videos": 0,
            "total_exercises": 0,
            "pct_mastery": 0,
        }

    # Add group data.  Allow a fake group "Ungrouped"
    for user in users:
        group_pk = getattr(user.group, "pk", None)
        if group_pk not in group_data:
            logging.error("User %s still in nonexistent group %s!" % (user.id, group_pk))
            continue
        group_data[group_pk]["total_users"] += 1
        group_data[group_pk]["total_logins"] += user_data[user.pk]["total_logins"]
        group_data[group_pk]["total_hours"] += user_data[user.pk]["total_hours"]
        group_data[group_pk]["total_videos"] += user_data[user.pk]["total_videos"]
        group_data[group_pk]["total_exercises"] += user_data[user.pk]["total_exercises"]

        total_mastery_so_far = (group_data[group_pk]["pct_mastery"] * (group_data[group_pk]["total_users"] - 1) + user_data[user.pk]["pct_mastery"])
        group_data[group_pk]["pct_mastery"] =  total_mastery_so_far / group_data[group_pk]["total_users"]

    if len(group_data) == 1 and group_data.has_key(None):
        if not group_data[None]["total_users"]:
            del group_data[None]

    return (user_data, group_data)
Example #51
0
def tabular_view(request, report_type="exercise"):
    """Tabular view also gets data server-side."""
    # important for setting the defaults for the coach nav bar

    language = lcode_to_django_lang(request.language)

    facility, group_id, context = coach_nav_context(request, "tabular")

    # Define how students are ordered--used to be as efficient as possible.
    student_ordering = ["last_name", "first_name", "username"]

    # Get a list of topics (sorted) and groups
    topics = [get_node_cache("Topic", language=language).get(tid["id"]) for tid in get_knowledgemap_topics(language=language) if report_type.title() in tid["contains"]]
    playlists = Playlist.all()

    (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(request, facility=facility)

    context.update(plotting_metadata_context(request, facility=facility))

    context.update({
        # For translators: the following two translations are nouns
        "report_types": ({"value": "exercise", "name":_("exercise")}, {"value": "video", "name": _("video")}),
        "request_report_type": report_type,
        "topics": [{"id": t["id"], "title": t["title"]} for t in topics if t],
        "playlists": [{"id": p.id, "title": p.title, "tag": p.tag} for p in playlists if p],
    })

    # get querystring info
    topic_id = request.GET.get("topic", "")
    playlist_id = request.GET.get("playlist", "")
    # No valid data; just show generic
    # Exactly one of topic_id or playlist_id should be present
    if not ((topic_id or playlist_id) and not (topic_id and playlist_id)):
        if playlists:
            messages.add_message(request, WARNING, _("Please select a playlist."))
        elif topics:
            messages.add_message(request, WARNING, _("Please select a topic."))
        return context

    playlist = (filter(lambda p: p.id == playlist_id, Playlist.all()) or [None])[0]

    if group_id:
        # Narrow by group
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(group__isnull=True, is_teacher=False)
            if facility:
                # filter only those ungrouped students for the facility
                users = users.filter(facility=facility)
                users = users.order_by(*student_ordering)
            else:
                # filter all ungroup students
                users = FacilityUser.objects.filter(group__isnull=True, is_teacher=False).order_by(*student_ordering)

        else:
            users = FacilityUser.objects.filter(
                group=group_id, is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [groups_dict["groups"] for groups_dict in groups if groups_dict["facility"] == facility.id]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility), is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None), is_teacher=False).order_by(*student_ordering)

    # We have enough data to render over a group of students
    # Get type-specific information
    if report_type == "exercise":
        # Fill in exercises
        if topic_id:
            exercises = get_topic_exercises(topic_id=topic_id)
        elif playlist:
            exercises = playlist.get_playlist_entries("Exercise", language=language)

        context["exercises"] = exercises

        # More code, but much faster
        exercise_names = [ex["id"] for ex in context["exercises"]]
        # Get students
        context["students"] = []
        exlogs = ExerciseLog.objects \
            .filter(user__in=users, exercise_id__in=exercise_names) \
            .order_by(*["user__%s" % field for field in student_ordering]) \
            .values("user__id", "struggling", "complete", "exercise_id")
        exlogs = list(exlogs)  # force the query to be evaluated

        exlog_idx = 0
        for user in users:
            log_table = {}
            while exlog_idx < len(exlogs) and exlogs[exlog_idx]["user__id"] == user.id:
                log_table[exlogs[exlog_idx]["exercise_id"]] = exlogs[exlog_idx]
                exlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "exercise_logs": log_table,
            })

    elif report_type == "video":
        # Fill in videos
        if topic_id:
            context["videos"] = get_topic_videos(topic_id=topic_id)
        elif playlist:
            context["videos"] = playlist.get_playlist_entries("Video", language=language)

        # More code, but much faster
        video_ids = [vid["id"] for vid in context["videos"]]
        # Get students
        context["students"] = []
        vidlogs = VideoLog.objects \
            .filter(user__in=users, video_id__in=video_ids) \
            .order_by(*["user__%s" % field for field in student_ordering])\
            .values("user__id", "complete", "video_id", "total_seconds_watched", "points")
        vidlogs = list(vidlogs)  # force the query to be executed now

        vidlog_idx = 0
        for user in users:
            log_table = {}
            while vidlog_idx < len(vidlogs) and vidlogs[vidlog_idx]["user__id"] == user.id:
                log_table[vidlogs[vidlog_idx]["video_id"]] = vidlogs[vidlog_idx]
                vidlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "video_logs": log_table,
            })
    else:
        raise Http404(_("Unknown report_type: %(report_type)s") % {"report_type": report_type})

    # Validate results by showing user messages.
    if not users:
        # 1. check group facility groups
        if len(groups) > 0 and not groups[0]['groups']:
            # 1. No groups available (for facility) and "no students" returned.
            messages.add_message(request, WARNING,
                                 _("No learner accounts have been created for selected facility/group."))
        elif topic_id and playlist_id:
            # 2. Both topic and playlist are selected.
            messages.add_message(request, WARNING, _("Please select either a topic or a playlist above, but not both."))
        elif not topic_id and not playlist_id:
            # 3. Group was selected, but data not queried because a topic or playlist was not selected.
            if playlists:
                # 4. No playlist was selected.
                messages.add_message(request, WARNING, _("Please select a playlist."))
            elif topics:
                # 5. No topic was selected.
                messages.add_message(request, WARNING, _("Please select a topic."))
        else:
            # 6. Everything specified, but no users fit the query.
            messages.add_message(request, WARNING, _("No learner accounts in this group have been created."))
    # End: Validate results by showing user messages.

    log_coach_report_view(request)

    return context
Example #52
0
def _get_user_usage_data(users, groups=None, period_start=None, period_end=None, group_id=None):
    """
    Returns facility user data, within the given date range.
    """

    groups = groups or set([user.group for user in users])

    # compute period start and end
    # Now compute stats, based on queried data
    num_exercises = len(get_node_cache('Exercise'))
    user_data = OrderedDict()
    group_data = OrderedDict()

    # Make queries efficiently
    exercise_logs = ExerciseLog.objects.filter(user__in=users, complete=True)
    video_logs = VideoLog.objects.filter(user__in=users, total_seconds_watched__gt=0)
    login_logs = UserLogSummary.objects.filter(user__in=users)

    # filter results
    login_logs = login_logs.filter(total_seconds__gt=0)
    if period_start:
        exercise_logs = exercise_logs.filter(completion_timestamp__gte=period_start)
    if period_end:
        # MUST: Fix the midnight bug where period end covers up to the prior day only because
        # period end is datetime(year, month, day, hour=0, minute=0), meaning midnight of previous day.
        # Example:
        #   If period_end == '2014-12-01', we cannot include the records dated '2014-12-01 09:30'.
        #   So to fix this, we change it to '2014-12-01 23:59.999999'.
        period_end = dateutil.parser.parse(period_end)
        period_end = period_end + dateutil.relativedelta.relativedelta(days=+1, microseconds=-1)
        exercise_logs = exercise_logs.filter(completion_timestamp__lte=period_end)
    if period_start and period_end:
        exercise_logs = exercise_logs.filter(Q(completion_timestamp__gte=period_start) &
                                             Q(completion_timestamp__lte=period_end))

        q1 = Q(completion_timestamp__isnull=False) & \
             Q(completion_timestamp__gte=period_start) & \
             Q(completion_timestamp__lte=period_end)
        q2 = Q(completion_timestamp__isnull=True)
        video_logs = video_logs.filter(q1 | q2)

        login_q1 = Q(start_datetime__gte=period_start) & Q(start_datetime__lte=period_end) & \
                   Q(end_datetime__gte=period_start) & Q(end_datetime__lte=period_end)
        login_logs = login_logs.filter(login_q1)
    # Force results in a single query
    exercise_logs = list(exercise_logs.values("exercise_id", "user__pk"))
    video_logs = list(video_logs.values("video_id", "user__pk"))
    login_logs = list(login_logs.values("activity_type", "total_seconds", "user__pk"))

    for user in users:
        user_data[user.pk] = OrderedDict()
        user_data[user.pk]["id"] = user.pk
        user_data[user.pk]["first_name"] = user.first_name
        user_data[user.pk]["last_name"] = user.last_name
        user_data[user.pk]["username"] = user.username
        user_data[user.pk]["group"] = user.group

        user_data[user.pk]["total_report_views"] = 0#report_stats["count__sum"] or 0
        user_data[user.pk]["total_logins"] =0# login_stats["count__sum"] or 0
        user_data[user.pk]["total_hours"] = 0#login_stats["total_seconds__sum"] or 0)/3600.

        user_data[user.pk]["total_exercises"] = 0
        user_data[user.pk]["pct_mastery"] = 0.
        user_data[user.pk]["exercises_mastered"] = []

        user_data[user.pk]["total_videos"] = 0
        user_data[user.pk]["videos_watched"] = []


    for elog in exercise_logs:
        user_data[elog["user__pk"]]["total_exercises"] += 1
        user_data[elog["user__pk"]]["pct_mastery"] += 1. / num_exercises
        user_data[elog["user__pk"]]["exercises_mastered"].append(elog["exercise_id"])

    for vlog in video_logs:
        user_data[vlog["user__pk"]]["total_videos"] += 1
        user_data[vlog["user__pk"]]["videos_watched"].append(vlog["video_id"])

    for llog in login_logs:
        if llog["activity_type"] == UserLog.get_activity_int("coachreport"):
            user_data[llog["user__pk"]]["total_report_views"] += 1
        elif llog["activity_type"] == UserLog.get_activity_int("login"):
            user_data[llog["user__pk"]]["total_hours"] += (llog["total_seconds"]) / 3600.
            user_data[llog["user__pk"]]["total_logins"] += 1

    for group in list(groups) + [None]*(group_id==None or group_id=="Ungrouped"):  # None for ungrouped, if no group_id passed.
        group_pk = getattr(group, "pk", None)
        group_name = getattr(group, "name", _("Ungrouped"))
        group_data[group_pk] = {
            "id": group_pk,
            "name": group_name,
            "total_logins": 0,
            "total_hours": 0,
            "total_users": 0,
            "total_videos": 0,
            "total_exercises": 0,
            "pct_mastery": 0,
        }

    # Add group data.  Allow a fake group "Ungrouped"
    for user in users:
        group_pk = getattr(user.group, "pk", None)
        if group_pk not in group_data:
            logging.error("User %s still in nonexistent group %s!" % (user.id, group_pk))
            continue
        group_data[group_pk]["total_users"] += 1
        group_data[group_pk]["total_logins"] += user_data[user.pk]["total_logins"]
        group_data[group_pk]["total_hours"] += user_data[user.pk]["total_hours"]
        group_data[group_pk]["total_videos"] += user_data[user.pk]["total_videos"]
        group_data[group_pk]["total_exercises"] += user_data[user.pk]["total_exercises"]

        total_mastery_so_far = (group_data[group_pk]["pct_mastery"] * (group_data[group_pk]["total_users"] - 1) + user_data[user.pk]["pct_mastery"])
        group_data[group_pk]["pct_mastery"] =  total_mastery_so_far / group_data[group_pk]["total_users"]

    if len(group_data) == 1 and group_data.has_key(None):
        if not group_data[None]["total_users"]:
            del group_data[None]

    return (user_data, group_data)