def user_progress(cls, user_id): """ Return a list of PlaylistProgress objects associated with the user. """ user = FacilityUser.objects.get(id=user_id) all_playlists = [getattr(pl, "__dict__", pl) for pl in Playlist.all() + get_leafed_topics()] # Retrieve video, exercise, and quiz logs that appear in this playlist user_vid_logs, user_ex_logs = cls.get_user_logs(user) exercise_ids = set([ex_log["exercise_id"] for ex_log in user_ex_logs]) video_ids = set([get_id2slug_map().get(vid_log["video_id"]) for vid_log in user_vid_logs]) quiz_log_ids = [ql_id["quiz"] for ql_id in QuizLog.objects.filter(user=user).values("quiz")] # Build a list of playlists for which the user has at least one data point ## TODO(dylanjbarth) this won't pick up playlists the user is assigned but has not started yet. user_playlists = list() for p in all_playlists: for e in (p.get("entries") or p.get("children")): if (e.get("entity_kind") or e.get("kind")) == "Video" or (e.get("entity_kind") or e.get("kind")) == "Exercise": entity_id = convert_leaf_url_to_id((e.get("entity_id") or e.get("id"))) if entity_id in exercise_ids or entity_id in video_ids: user_playlists.append(p) break elif e.get("entity_kind") == "Quiz": if p.get("id") in quiz_log_ids: user_playlists.append(p) # Store stats for each playlist user_progress = list() for i, p in enumerate(user_playlists): # Playlist entry totals pl_video_ids, pl_exercise_ids = cls.get_playlist_entry_ids(p) n_pl_videos = float(len(pl_video_ids)) n_pl_exercises = float(len(pl_exercise_ids)) # Vid & exercise logs in this playlist pl_ex_logs = [ex_log for ex_log in user_ex_logs if ex_log["exercise_id"] in pl_exercise_ids] pl_vid_logs = [vid_log for vid_log in user_vid_logs if vid_log["video_id"] in pl_video_ids] # Compute video stats n_vid_complete = len([vid for vid in pl_vid_logs if vid["complete"]]) n_vid_started = len([vid for vid in pl_vid_logs if (vid["total_seconds_watched"] > 0) and (not vid["complete"])]) vid_pct_complete = int(float(n_vid_complete) / n_pl_videos * 100) if n_pl_videos else 0 vid_pct_started = int(float(n_vid_started) / n_pl_videos * 100) if n_pl_videos else 0 if vid_pct_complete == 100: vid_status = "complete" elif n_vid_started > 0: vid_status = "inprogress" else: vid_status = "notstarted" # Compute exercise stats n_ex_mastered = len([ex for ex in pl_ex_logs if ex["complete"]]) n_ex_started = len([ex for ex in pl_ex_logs if ex["attempts"] > 0]) n_ex_incomplete = len([ex for ex in pl_ex_logs if (ex["attempts"] > 0 and not ex["complete"])]) n_ex_struggling = len([ex for ex in pl_ex_logs if ex["struggling"]]) ex_pct_mastered = int(float(n_ex_mastered) / n_pl_exercises * 100) ex_pct_incomplete = int(float(n_ex_incomplete) / n_pl_exercises * 100) ex_pct_struggling = int(float(n_ex_struggling) / n_pl_exercises * 100) if not n_ex_started: ex_status = "notstarted" elif ex_pct_struggling > 0: # note: we want to help students prioritize areas they need to focus on # therefore if they are struggling in this exercise group, we highlight it for them ex_status = "struggling" elif ex_pct_mastered < 99: ex_status = "inprogress" else: ex_status = "complete" # Compute quiz stats quiz_exists, quiz_log, quiz_pct_score = cls.get_quiz_log(user, (p.get("entries") or p.get("children")), p.get("id")) if quiz_log: if quiz_pct_score <= 50: quiz_status = "struggling" elif quiz_pct_score <= 79: quiz_status = "borderline" else: quiz_status = "complete" else: quiz_status = "notstarted" progress = { "title": p.get("title"), "id": p.get("id"), "tag": p.get("tag"), "vid_pct_complete": vid_pct_complete, "vid_pct_started": vid_pct_started, "vid_status": vid_status, "ex_pct_mastered": ex_pct_mastered, "ex_pct_incomplete": ex_pct_incomplete, "ex_pct_struggling": ex_pct_struggling, "ex_status": ex_status, "quiz_status": quiz_status, "quiz_exists": quiz_exists, "quiz_pct_score": quiz_pct_score, "n_pl_videos": n_pl_videos, "n_pl_exercises": n_pl_exercises, } try: progress["url"] = reverse("view_playlist", kwargs={"playlist_id": p.get("id")}) except NoReverseMatch: progress["url"] = reverse("learn") + p.get("path") user_progress.append(cls(**progress)) return user_progress
def show_logs(request, ndays=None): """Show file-based logging info for video downloads, language packs, and subtitles""" ndays = ndays or int(request.GET.get("days", 7)) def get_logger_filename(logger_type): return stats_logger(logger_type).handlers[0].baseFilename def parse_data(logger_type, data_fields, windowsize=128, ndays=None): parsed_data = {} nparts = len(data_fields) summary_data = dict([(fld, {}) for fld in (data_fields + ["date"])]) filepath = get_logger_filename(logger_type) if not os.path.exists(filepath): return (parsed_data, summary_data) # Group by ip, date, and youtube_id old_data = "" first_loop = True last_loop = False with open(filepath, "r") as fp: fp.seek(0, 2) # go to the end of the stream while True: # Read the next chunk of data try: # Get the data try: if first_loop: fp.seek(-windowsize, 1) # go backwards by a few first_loop = False else: fp.seek(-2 * windowsize, 1) # go backwards by a few cur_data = fp.read(windowsize) + old_data except: if last_loop and not old_data: raise elif last_loop: cur_data = old_data old_data = "" else: last_loop = True fp.seek(0) cur_data = fp.read(windowsize) + old_data # could be some overlap... if not cur_data: break; except: break # Parse the data lines = cur_data.split("\n") old_data = lines[0] if len(lines) > 1 else "" new_data = lines[1:] if len(lines) > 1 else lines for l in new_data: if not l: continue # All start with a date parts = l.split(" - ", 2) if len(parts) != 2: continue tim = parts[0] dat = tim.split(" ")[0] # Validate that this date is within the accepted range parsed_date = datetime.datetime.strptime(dat, "%Y-%m-%d") #logging.debug("%s %s" % (parsed_date, (datetime.datetime.now() - timedelta(days=ndays)))) if ndays is not None and datetime.datetime.now() - timedelta(days=ndays) > parsed_date: last_loop = True old_data = "" break; # The rest is semicolon-delimited parts = parts[1].split(";") # vd;127.0.0.1;xvnpSRO9IDM # Now save things off parsed_data[tim] = dict([(data_fields[idx], parts[idx]) for idx in range(nparts)]) summary_data["date"][dat] = 1 + summary_data["date"].get(dat, 0) for idx in range(nparts): summary_data[data_fields[idx]][parts[idx]] = 1 + summary_data[data_fields[idx]].get(parts[idx], 0) for key, val in summary_data.iteritems(): summary_data[key] = sorted_dict(val, key=lambda t: t[0]) return (parsed_data, summary_data) (video_raw_data, video_summary_data) = parse_data("videos", ["task_id", "ip_address", "youtube_id"], ndays=ndays) (lp_raw_data, lp_summary_data) = parse_data("language_packs", ["task_id", "ip_address", "lang_code", "version"], ndays=ndays) (srt_raw_data, srt_summary_data) = parse_data("subtitles", ["task_id", "ip_address", "lang_code", "youtube_id"], ndays=ndays) return { "ndays": ndays, "videos": { "raw": video_raw_data, "dates": video_summary_data["date"], "ips": video_summary_data["ip_address"], "slugs": sum_counter(video_summary_data["youtube_id"], fn=lambda yid: get_id2slug_map().get(get_video_id(yid))), "lang_codes": sum_counter(video_summary_data["youtube_id"], fn=lambda yid: get_video_language(yid)), }, "language_packs": { "raw": lp_raw_data, "dates": lp_summary_data["date"], "ips": lp_summary_data["ip_address"], "lang_codes": lp_summary_data["lang_code"], "versions": lp_summary_data["version"], }, "subtitles": { "raw": srt_raw_data, "dates": srt_summary_data["date"], "ips": srt_summary_data["ip_address"], "lang_codes": srt_summary_data["lang_code"], }, }
def user_progress(cls, user_id, language=None): """ Return a list of PlaylistProgress objects associated with the user. """ if not language: language = Settings.get("default_language") or settings.LANGUAGE_CODE user = FacilityUser.objects.get(id=user_id) all_playlists = get_leafed_topics(language=language) # Retrieve video, exercise, and quiz logs that appear in this playlist user_vid_logs, user_ex_logs = cls.get_user_logs(user) exercise_ids = set([ex_log["exercise_id"] for ex_log in user_ex_logs]) video_ids = set([get_id2slug_map().get(vid_log["video_id"]) for vid_log in user_vid_logs]) # quiz_log_ids = [ql_id["quiz"] for ql_id in QuizLog.objects.filter(user=user).values("quiz")] # Build a list of playlists for which the user has at least one data point user_playlists = list() for p in all_playlists: for e_id in p.get("children"): if e_id in exercise_ids or e_id in video_ids: user_playlists.append(p) break # Store stats for each playlist user_progress = list() for i, p in enumerate(user_playlists): # Playlist entry totals pl_video_ids, pl_exercise_ids = cls.get_playlist_entry_ids(p) n_pl_videos = float(len(pl_video_ids)) n_pl_exercises = float(len(pl_exercise_ids)) # Vid & exercise logs in this playlist pl_ex_logs = [ex_log for ex_log in user_ex_logs if ex_log["exercise_id"] in pl_exercise_ids] pl_vid_logs = [vid_log for vid_log in user_vid_logs if vid_log["video_id"] in pl_video_ids] # Compute video stats n_vid_complete = len([vid for vid in pl_vid_logs if vid["complete"]]) n_vid_started = len([vid for vid in pl_vid_logs if (vid["total_seconds_watched"] > 0) and (not vid["complete"])]) vid_pct_complete = int(float(n_vid_complete) / n_pl_videos * 100) if n_pl_videos else 0 vid_pct_started = int(float(n_vid_started) / n_pl_videos * 100) if n_pl_videos else 0 if vid_pct_complete == 100: vid_status = "complete" elif n_vid_started > 0: vid_status = "inprogress" else: vid_status = "notstarted" # Compute exercise stats n_ex_mastered = len([ex for ex in pl_ex_logs if ex["complete"]]) n_ex_started = len([ex for ex in pl_ex_logs if ex["attempts"] > 0]) n_ex_incomplete = len([ex for ex in pl_ex_logs if (ex["attempts"] > 0 and not ex["complete"])]) n_ex_struggling = len([ex for ex in pl_ex_logs if ex["struggling"]]) ex_pct_mastered = int(float(n_ex_mastered) / (n_pl_exercises or 1) * 100) ex_pct_incomplete = int(float(n_ex_incomplete) / (n_pl_exercises or 1) * 100) ex_pct_struggling = int(float(n_ex_struggling) / (n_pl_exercises or 1) * 100) if not n_ex_started: ex_status = "notstarted" elif ex_pct_struggling > 0: # note: we want to help students prioritize areas they need to focus on # therefore if they are struggling in this exercise group, we highlight it for them ex_status = "struggling" elif ex_pct_mastered < 99: ex_status = "inprogress" else: ex_status = "complete" # Oh Quizzes, we hardly knew ye! # TODO (rtibbles): Sort out the status of Quizzes, and either reinstate them or remove them. # Compute quiz stats # quiz_exists, quiz_log, quiz_pct_score = cls.get_quiz_log(user, (p.get("entries") or p.get("children")), p.get("id")) # if quiz_log: # if quiz_pct_score <= 50: # quiz_status = "struggling" # elif quiz_pct_score <= 79: # quiz_status = "borderline" # else: # quiz_status = "complete" # else: # quiz_status = "notstarted" progress = { "title": p.get("title"), "id": p.get("id"), "tag": p.get("tag"), "vid_pct_complete": vid_pct_complete, "vid_pct_started": vid_pct_started, "vid_status": vid_status, "ex_pct_mastered": ex_pct_mastered, "ex_pct_incomplete": ex_pct_incomplete, "ex_pct_struggling": ex_pct_struggling, "ex_status": ex_status, # "quiz_status": quiz_status, # "quiz_exists": quiz_exists, # "quiz_pct_score": quiz_pct_score, "n_pl_videos": n_pl_videos, "n_pl_exercises": n_pl_exercises, } try: progress["url"] = reverse("view_playlist", kwargs={"playlist_id": p.get("id")}) except NoReverseMatch: progress["url"] = reverse("learn") + p.get("path") user_progress.append(cls(**progress)) return user_progress
def show_logs(request, ndays=None): """Show file-based logging info for video downloads, language packs, and subtitles""" ndays = ndays or int(request.GET.get("days", 7)) def get_logger_filename(logger_type): return stats_logger(logger_type).handlers[0].baseFilename def parse_data(logger_type, data_fields, windowsize=128, ndays=None): parsed_data = {} nparts = len(data_fields) summary_data = dict([(fld, {}) for fld in (data_fields + ["date"])]) filepath = get_logger_filename(logger_type) if not os.path.exists(filepath): return (parsed_data, summary_data) # Group by ip, date, and youtube_id old_data = "" first_loop = True last_loop = False with open(filepath, "r") as fp: fp.seek(0, 2) # go to the end of the stream while True: # Read the next chunk of data try: # Get the data try: if first_loop: fp.seek(-windowsize, 1) # go backwards by a few first_loop = False else: fp.seek(-2 * windowsize, 1) # go backwards by a few cur_data = fp.read(windowsize) + old_data except: if last_loop and not old_data: raise elif last_loop: cur_data = old_data old_data = "" else: last_loop = True fp.seek(0) cur_data = fp.read( windowsize ) + old_data # could be some overlap... if not cur_data: break except: break # Parse the data lines = cur_data.split("\n") old_data = lines[0] if len(lines) > 1 else "" new_data = lines[1:] if len(lines) > 1 else lines for l in new_data: if not l: continue # All start with a date parts = l.split(" - ", 2) if len(parts) != 2: continue tim = parts[0] dat = tim.split(" ")[0] # Validate that this date is within the accepted range parsed_date = datetime.datetime.strptime(dat, "%Y-%m-%d") #logging.debug("%s %s" % (parsed_date, (datetime.datetime.now() - timedelta(days=ndays)))) if ndays is not None and datetime.datetime.now( ) - timedelta(days=ndays) > parsed_date: last_loop = True old_data = "" break # The rest is semicolon-delimited parts = parts[1].split(";") # vd;127.0.0.1;xvnpSRO9IDM # Now save things off parsed_data[tim] = dict([(data_fields[idx], parts[idx]) for idx in range(nparts)]) summary_data["date"][dat] = 1 + summary_data["date"].get( dat, 0) for idx in range(nparts): summary_data[data_fields[idx]][parts[ idx]] = 1 + summary_data[data_fields[idx]].get( parts[idx], 0) for key, val in summary_data.iteritems(): summary_data[key] = sorted_dict(val, key=lambda t: t[0]) return (parsed_data, summary_data) (video_raw_data, video_summary_data) = parse_data("videos", ["task_id", "ip_address", "youtube_id"], ndays=ndays) (lp_raw_data, lp_summary_data) = parse_data( "language_packs", ["task_id", "ip_address", "lang_code", "version"], ndays=ndays) (srt_raw_data, srt_summary_data) = parse_data( "subtitles", ["task_id", "ip_address", "lang_code", "youtube_id"], ndays=ndays) return { "ndays": ndays, "videos": { "raw": video_raw_data, "dates": video_summary_data["date"], "ips": video_summary_data["ip_address"], "slugs": sum_counter( video_summary_data["youtube_id"], fn=lambda yid: get_id2slug_map().get(get_video_id(yid))), "lang_codes": sum_counter(video_summary_data["youtube_id"], fn=lambda yid: get_video_language(yid)), }, "language_packs": { "raw": lp_raw_data, "dates": lp_summary_data["date"], "ips": lp_summary_data["ip_address"], "lang_codes": lp_summary_data["lang_code"], "versions": lp_summary_data["version"], }, "subtitles": { "raw": srt_raw_data, "dates": srt_summary_data["date"], "ips": srt_summary_data["ip_address"], "lang_codes": srt_summary_data["lang_code"], }, }
"""Classes used by the student progress tastypie API""" import json from django.core.urlresolvers import reverse, NoReverseMatch from django.core.exceptions import ObjectDoesNotExist from kalite.facility.models import FacilityUser from kalite.main.models import ExerciseLog, VideoLog from kalite.playlist.models import VanillaPlaylist as Playlist, QuizLog from kalite.topic_tools import get_slug2id_map, get_id2slug_map, convert_leaf_url_to_id, get_leafed_topics, get_content_cache, get_exercise_cache ID2SLUG_MAP = get_id2slug_map() SLUG2ID_MAP = get_slug2id_map() class PlaylistProgressParent: """Parent class for helpful class methods""" @classmethod 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("entries") or playlist.get("children") # TODO(dylanjbarth): 0.13 playlist entities shouldn't have the /v or /e in them at all. pl_video_ids = set([ SLUG2ID_MAP.get(entry.get("entity_id")) or entry.get("id") for entry in playlist_entries if entry.get("entity_kind") == "Video" ]) pl_exercise_ids = set([ entry.get("entity_id") or entry.get("id") for entry in playlist_entries if (entry.get("entity_kind") or entry.get("kind")) == "Exercise"
def user_progress(cls, user_id, language=None): """ Return a list of PlaylistProgress objects associated with the user. """ if not language: language = Settings.get( "default_language") or settings.LANGUAGE_CODE user = FacilityUser.objects.get(id=user_id) all_playlists = get_leafed_topics(language=language) # Retrieve video, exercise, and quiz logs that appear in this playlist user_vid_logs, user_ex_logs = cls.get_user_logs(user) exercise_ids = set([ex_log["exercise_id"] for ex_log in user_ex_logs]) video_ids = set([ get_id2slug_map().get(vid_log["video_id"]) for vid_log in user_vid_logs ]) # quiz_log_ids = [ql_id["quiz"] for ql_id in QuizLog.objects.filter(user=user).values("quiz")] # Build a list of playlists for which the user has at least one data point user_playlists = list() for p in all_playlists: for e_id in p.get("children"): if e_id in exercise_ids or e_id in video_ids: user_playlists.append(p) break # Store stats for each playlist user_progress = list() for i, p in enumerate(user_playlists): # Playlist entry totals pl_video_ids, pl_exercise_ids = cls.get_playlist_entry_ids(p) n_pl_videos = float(len(pl_video_ids)) n_pl_exercises = float(len(pl_exercise_ids)) # Vid & exercise logs in this playlist pl_ex_logs = [ ex_log for ex_log in user_ex_logs if ex_log["exercise_id"] in pl_exercise_ids ] pl_vid_logs = [ vid_log for vid_log in user_vid_logs if vid_log["video_id"] in pl_video_ids ] # Compute video stats n_vid_complete = len( [vid for vid in pl_vid_logs if vid["complete"]]) n_vid_started = len([ vid for vid in pl_vid_logs if (vid["total_seconds_watched"] > 0) and (not vid["complete"]) ]) vid_pct_complete = int(float(n_vid_complete) / n_pl_videos * 100) if n_pl_videos else 0 vid_pct_started = int(float(n_vid_started) / n_pl_videos * 100) if n_pl_videos else 0 if vid_pct_complete == 100: vid_status = "complete" elif n_vid_started > 0: vid_status = "inprogress" else: vid_status = "notstarted" # Compute exercise stats n_ex_mastered = len([ex for ex in pl_ex_logs if ex["complete"]]) n_ex_started = len([ex for ex in pl_ex_logs if ex["attempts"] > 0]) n_ex_incomplete = len([ ex for ex in pl_ex_logs if (ex["attempts"] > 0 and not ex["complete"]) ]) n_ex_struggling = len( [ex for ex in pl_ex_logs if ex["struggling"]]) ex_pct_mastered = int( float(n_ex_mastered) / (n_pl_exercises or 1) * 100) ex_pct_incomplete = int( float(n_ex_incomplete) / (n_pl_exercises or 1) * 100) ex_pct_struggling = int( float(n_ex_struggling) / (n_pl_exercises or 1) * 100) if not n_ex_started: ex_status = "notstarted" elif ex_pct_struggling > 0: # note: we want to help students prioritize areas they need to focus on # therefore if they are struggling in this exercise group, we highlight it for them ex_status = "struggling" elif ex_pct_mastered < 99: ex_status = "inprogress" else: ex_status = "complete" # Oh Quizzes, we hardly knew ye! # TODO (rtibbles): Sort out the status of Quizzes, and either reinstate them or remove them. # Compute quiz stats # quiz_exists, quiz_log, quiz_pct_score = cls.get_quiz_log(user, (p.get("entries") or p.get("children")), p.get("id")) # if quiz_log: # if quiz_pct_score <= 50: # quiz_status = "struggling" # elif quiz_pct_score <= 79: # quiz_status = "borderline" # else: # quiz_status = "complete" # else: # quiz_status = "notstarted" progress = { "title": p.get("title"), "id": p.get("id"), "tag": p.get("tag"), "vid_pct_complete": vid_pct_complete, "vid_pct_started": vid_pct_started, "vid_status": vid_status, "ex_pct_mastered": ex_pct_mastered, "ex_pct_incomplete": ex_pct_incomplete, "ex_pct_struggling": ex_pct_struggling, "ex_status": ex_status, # "quiz_status": quiz_status, # "quiz_exists": quiz_exists, # "quiz_pct_score": quiz_pct_score, "n_pl_videos": n_pl_videos, "n_pl_exercises": n_pl_exercises, } try: progress["url"] = reverse("view_playlist", kwargs={"playlist_id": p.get("id")}) except NoReverseMatch: progress["url"] = reverse("learn") + p.get("path") user_progress.append(cls(**progress)) return user_progress
def user_progress(cls, user_id): """ Return a list of PlaylistProgress objects associated with the user. """ user = FacilityUser.objects.get(id=user_id) all_playlists = [getattr(pl, "__dict__", pl) for pl in Playlist.all() + get_leafed_topics()] # Retrieve video, exercise, and quiz logs that appear in this playlist user_vid_logs, user_ex_logs = cls.get_user_logs(user) exercise_ids = set([ex_log["exercise_id"] for ex_log in user_ex_logs]) video_ids = set([get_id2slug_map().get(vid_log["video_id"]) for vid_log in user_vid_logs]) quiz_log_ids = [ql_id["quiz"] for ql_id in QuizLog.objects.filter(user=user).values("quiz")] # Build a list of playlists for which the user has at least one data point ## TODO(dylanjbarth) this won't pick up playlists the user is assigned but has not started yet. user_playlists = list() for p in all_playlists: for e in p.get("entries") or p.get("children"): if (e.get("entity_kind") or e.get("kind")) == "Video" or ( e.get("entity_kind") or e.get("kind") ) == "Exercise": entity_id = convert_leaf_url_to_id((e.get("entity_id") or e.get("id"))) if entity_id in exercise_ids or entity_id in video_ids: user_playlists.append(p) break elif e.get("entity_kind") == "Quiz": if p.get("id") in quiz_log_ids: user_playlists.append(p) # Store stats for each playlist user_progress = list() for i, p in enumerate(user_playlists): # Playlist entry totals pl_video_ids, pl_exercise_ids = cls.get_playlist_entry_ids(p) n_pl_videos = float(len(pl_video_ids)) n_pl_exercises = float(len(pl_exercise_ids)) # Vid & exercise logs in this playlist pl_ex_logs = [ex_log for ex_log in user_ex_logs if ex_log["exercise_id"] in pl_exercise_ids] pl_vid_logs = [vid_log for vid_log in user_vid_logs if vid_log["video_id"] in pl_video_ids] # Compute video stats n_vid_complete = len([vid for vid in pl_vid_logs if vid["complete"]]) n_vid_started = len( [vid for vid in pl_vid_logs if (vid["total_seconds_watched"] > 0) and (not vid["complete"])] ) vid_pct_complete = int(float(n_vid_complete) / n_pl_videos * 100) if n_pl_videos else 0 vid_pct_started = int(float(n_vid_started) / n_pl_videos * 100) if n_pl_videos else 0 if vid_pct_complete == 100: vid_status = "complete" elif n_vid_started > 0: vid_status = "inprogress" else: vid_status = "notstarted" # Compute exercise stats n_ex_mastered = len([ex for ex in pl_ex_logs if ex["complete"]]) n_ex_started = len([ex for ex in pl_ex_logs if ex["attempts"] > 0]) n_ex_incomplete = len([ex for ex in pl_ex_logs if (ex["attempts"] > 0 and not ex["complete"])]) n_ex_struggling = len([ex for ex in pl_ex_logs if ex["struggling"]]) ex_pct_mastered = int(float(n_ex_mastered) / n_pl_exercises * 100) ex_pct_incomplete = int(float(n_ex_incomplete) / n_pl_exercises * 100) ex_pct_struggling = int(float(n_ex_struggling) / n_pl_exercises * 100) if not n_ex_started: ex_status = "notstarted" elif ex_pct_struggling > 0: # note: we want to help students prioritize areas they need to focus on # therefore if they are struggling in this exercise group, we highlight it for them ex_status = "struggling" elif ex_pct_mastered < 99: ex_status = "inprogress" else: ex_status = "complete" # Compute quiz stats quiz_exists, quiz_log, quiz_pct_score = cls.get_quiz_log( user, (p.get("entries") or p.get("children")), p.get("id") ) if quiz_log: if quiz_pct_score <= 50: quiz_status = "struggling" elif quiz_pct_score <= 79: quiz_status = "borderline" else: quiz_status = "complete" else: quiz_status = "notstarted" progress = { "title": p.get("title"), "id": p.get("id"), "tag": p.get("tag"), "vid_pct_complete": vid_pct_complete, "vid_pct_started": vid_pct_started, "vid_status": vid_status, "ex_pct_mastered": ex_pct_mastered, "ex_pct_incomplete": ex_pct_incomplete, "ex_pct_struggling": ex_pct_struggling, "ex_status": ex_status, "quiz_status": quiz_status, "quiz_exists": quiz_exists, "quiz_pct_score": quiz_pct_score, "n_pl_videos": n_pl_videos, "n_pl_exercises": n_pl_exercises, } try: progress["url"] = reverse("view_playlist", kwargs={"playlist_id": p.get("id")}) except NoReverseMatch: progress["url"] = reverse("learn") + p.get("path") user_progress.append(cls(**progress)) return user_progress