def _setup(self, num_logs=50, **kwargs): super(OneHundredRandomLogUpdates, self)._setup(**kwargs) 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["Video"].keys())) vid_id = NODE_CACHE["Video"].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()
def setUp(self): super(TestExerciseLogs, self).setUp() # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username="******", facility=self.facility) self.user.set_password("dumber") self.user.save() # create an initial ExerciseLog instance so we have something to collide with later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was saved as intended self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.")
def handle(self, *args, **options): if settings.CENTRAL_SERVER: raise CommandError("Don't run this on the central server!! Data not linked to any zone on the central server is BAD.") facility = Facility(name="Wilson Elementary") facility.save() group1 = FacilityGroup(facility=facility, name="Class 4E") group1.full_clean() group1.save() group2 = FacilityGroup(facility=facility, name="Class 5B") group2.full_clean() group2.save() facilityusers = [] for i in range(0,10): newuser1 = FacilityUser(facility=facility, username=usernames[i], first_name=firstnames[i], last_name=lastnames[i], group=group1) if settings.DEBUG: newuser1.set_password(hashed_password = hashed_blah)#"blah") else: newuser1.set_password("blah") newuser1.full_clean() newuser1.save() facilityusers.append(newuser1) newuser2 = FacilityUser(facility=facility, username=usernames[i+10], first_name=firstnames[i+10], last_name=lastnames[i+10], group=group2) if settings.DEBUG: newuser2.set_password(hashed_password = hashed_blah)#"blah") else: newuser2.set_password("blah") newuser2.full_clean() newuser2.save() facilityusers.append(newuser2) for topic in topics: exercises = get_topic_exercises(topic_id=topic) exercises_a = [random.random() for i in range(len(exercises))] exercises_b = [float(i) / len(exercises) for i in range(len(exercises))] for i, user in enumerate(facilityusers): for j, exercise in enumerate(exercises): sig = sigmoid(proficiency[i], exercises_a[j], exercises_b[j]) if random.random() < 0.05 * (1-sig) and j > 2: break if random.random() < 0.15: continue attempts = random.random() * 40 + 10 streak_progress = max(10, min(100, 10 * random.random() + 10 * attempts * sig)) print int(attempts), int(streak_progress), user.first_name, exercise["name"] log = ExerciseLog(user=user, exercise_id=exercise["name"], attempts=attempts, streak_progress=streak_progress) log.full_clean() log.save()
def test_exerciselog_collision(self): # create a new exercise log with the same exercise_id and user, but different points/attempts exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) exerciselog.points = self.NEW_POINTS exerciselog.attempts = self.NEW_ATTEMPTS # try saving the new ExerciseLog: this is where the collision will happen, hopefully leading to a merge exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog2 = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog has been properly merged self.assertEqual(exerciselog.points, max(self.ORIGINAL_POINTS, self.NEW_POINTS), "The ExerciseLog's points were not properly merged.") self.assertEqual(exerciselog.attempts, max(self.ORIGINAL_ATTEMPTS, self.NEW_ATTEMPTS), "The ExerciseLog's attempts have already changed.")
def setUp(self): # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username=self.USERNAME, facility=self.facility) self.user.set_password(self.PASSWORD) self.user.save() # create an initial ExerciseLog instance so we have something to update later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.streak_progress = self.ORIGINAL_STREAK_PROGRESS self.original_exerciselog.save()
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, youtube_id=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: %s" % 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: %s" % e return JsonResponseMessageError(error_message) return JsonResponse({"success": "Uploaded %d exercises and %d videos" % (n_exercises_uploaded, n_videos_uploaded)})
def handle(self, *args, **options): facility = Facility(name="Wilson Elementary") facility.save() group1 = FacilityGroup(facility=facility, name="Class 4E") group1.full_clean() group1.save() group2 = FacilityGroup(facility=facility, name="Class 5B") group2.full_clean() group2.save() facilityusers = [] for i in range(0,10): newuser1 = FacilityUser(facility=facility, username=usernames[i], first_name=firstnames[i], last_name=lastnames[i], group=group1) newuser1.set_password("blah") newuser1.full_clean() newuser1.save() facilityusers.append(newuser1) newuser2 = FacilityUser(facility=facility, username=usernames[i+10], first_name=firstnames[i+10], last_name=lastnames[i+10], group=group2) newuser2.set_password("blah") newuser2.full_clean() newuser2.save() facilityusers.append(newuser2) for topic in topics: exercises = json.load(open("./static/data/topicdata/" + topic + ".json","r")) exercises = sorted(exercises, key = lambda k: (k["h_position"], k["v_position"])) exercises_a = [random.random() for i in range(len(exercises))] exercises_b = [float(i) / len(exercises) for i in range(len(exercises))] for i, user in enumerate(facilityusers): for j, exercise in enumerate(exercises): sig = sigmoid(proficiency[i], exercises_a[j], exercises_b[j]) if random.random() < 0.05 * (1-sig) and j > 2: break if random.random() < 0.15: continue attempts = random.random() * 40 + 10 streak_progress = max(10, min(100, 10 * random.random() + 10 * attempts * sig)) print int(attempts), int(streak_progress), user.first_name, exercise["name"] log = ExerciseLog(user=user, exercise_id=exercise["name"], attempts=attempts, streak_progress=streak_progress) log.full_clean() log.save()
def status(request): """In order to promote (efficient) caching on (low-powered) distributed devices, we do not include ANY user data in our templates. Instead, an AJAX request is made to download user data, and javascript used to update the page. This view is the view providing the json blob of user information, for each page view on the distributed server. Besides basic user data, we also provide access to the Django message system through this API, again to promote caching by excluding any dynamic information from the server-generated templates. """ # Build a list of messages to pass to the user. # Iterating over the messages removes them from the # session storage, thus they only appear once. message_dicts = [] for message in get_messages(request): # Make sure to escape strings not marked as safe. # Note: this duplicates a bit of Django template logic. msg_txt = message.message if not (isinstance(msg_txt, SafeString) or isinstance(msg_txt, SafeUnicode)): msg_txt = cgi.escape(str(msg_txt)) message_dicts.append({ "tags": message.tags, "text": msg_txt, }) # Default data data = { "is_logged_in": request.is_logged_in, "registered": bool(Settings.get("registered")), "is_admin": request.is_admin, "is_django_user": request.is_django_user, "points": 0, "messages": message_dicts, } # Override properties using facility data if "facility_user" in request.session: user = request.session["facility_user"] data["is_logged_in"] = True data["username"] = user.get_name() data["points"] = VideoLog.get_points_for_user( user) + ExerciseLog.get_points_for_user(user) # Override data using django data if request.user.is_authenticated(): data["is_logged_in"] = True data["username"] = request.user.username return JsonResponse(data)
def status(request): """In order to promote (efficient) caching on (low-powered) distributed devices, we do not include ANY user data in our templates. Instead, an AJAX request is made to download user data, and javascript used to update the page. This view is the view providing the json blob of user information, for each page view on the distributed server. Besides basic user data, we also provide access to the Django message system through this API, again to promote caching by excluding any dynamic information from the server-generated templates. """ # Build a list of messages to pass to the user. # Iterating over the messages removes them from the # session storage, thus they only appear once. message_dicts = [] for message in get_messages(request): # Make sure to escape strings not marked as safe. # Note: this duplicates a bit of Django template logic. msg_txt = message.message if not (isinstance(msg_txt, SafeString) or isinstance(msg_txt, SafeUnicode)): msg_txt = cgi.escape(str(msg_txt)) message_dicts.append({ "tags": message.tags, "text": msg_txt, }) # Default data data = { "is_logged_in": request.is_logged_in, "registered": bool(Settings.get("registered")), "is_admin": request.is_admin, "is_django_user": request.is_django_user, "points": 0, "messages": message_dicts, } # Override properties using facility data if "facility_user" in request.session: user = request.session["facility_user"] data["is_logged_in"] = True data["username"] = user.get_name() data["points"] = VideoLog.get_points_for_user(user) + ExerciseLog.get_points_for_user(user) # Override data using django data if request.user.is_authenticated(): data["is_logged_in"] = True data["username"] = request.user.username return JsonResponse(data)
def status(request): data = { "is_logged_in": request.is_logged_in, "registered": bool(Settings.get("registered")), "is_admin": request.is_admin, "is_django_user": request.is_django_user, "points": 0, } if "facility_user" in request.session: user = request.session["facility_user"] data["is_logged_in"] = True data["username"] = user.get_name() data["points"] = VideoLog.get_points_for_user(user) + ExerciseLog.get_points_for_user(user) if request.user.is_authenticated(): data["is_logged_in"] = True data["username"] = request.user.username return JsonResponse(data)
def status(request): data = { "is_logged_in": request.is_logged_in, "registered": bool(Settings.get("registered")), "is_admin": request.is_admin, "is_django_user": request.is_django_user, "points": 0, } if "facility_user" in request.session: user = request.session["facility_user"] data["is_logged_in"] = True data["username"] = user.get_name() data["points"] = VideoLog.get_points_for_user( user) + ExerciseLog.get_points_for_user(user) if request.user.is_authenticated(): data["is_logged_in"] = True data["username"] = request.user.username return JsonResponse(data)
def setUp(self): # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username="******", password="******", facility=self.facility) self.user.save() # create an initial ExerciseLog instance so we have something to collide with later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was saved as intended self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.")
def handle(self, *args, **options): facility = Facility(name="Wilson Elementary") facility.save() group1 = FacilityGroup(facility=facility, name="Class 4E") group1.full_clean() group1.save() group2 = FacilityGroup(facility=facility, name="Class 5B") group2.full_clean() group2.save() facilityusers = [] for i in range(0,10): newuser1 = FacilityUser(facility=facility, username=usernames[i], first_name=firstnames[i], last_name=lastnames[i], group=group1) if settings.DEBUG: newuser1.set_password(hashed_password = hashed_blah)#"blah") else: newuser1.set_password("blah") newuser1.full_clean() newuser1.save() facilityusers.append(newuser1) newuser2 = FacilityUser(facility=facility, username=usernames[i+10], first_name=firstnames[i+10], last_name=lastnames[i+10], group=group2) if settings.DEBUG: newuser2.set_password(hashed_password = hashed_blah)#"blah") else: newuser2.set_password("blah") newuser2.full_clean() newuser2.save() facilityusers.append(newuser2) for topic in topics: exercises = get_topic_exercises(topic_id=topic) exercises_a = [random.random() for i in range(len(exercises))] exercises_b = [float(i) / len(exercises) for i in range(len(exercises))] for i, user in enumerate(facilityusers): for j, exercise in enumerate(exercises): sig = sigmoid(proficiency[i], exercises_a[j], exercises_b[j]) if random.random() < 0.05 * (1-sig) and j > 2: break if random.random() < 0.15: continue attempts = random.random() * 40 + 10 streak_progress = max(10, min(100, 10 * random.random() + 10 * attempts * sig)) print int(attempts), int(streak_progress), user.first_name, exercise["name"] log = ExerciseLog(user=user, exercise_id=exercise["name"], attempts=attempts, streak_progress=streak_progress) log.full_clean() log.save()
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"]) # Save videos n_videos_uploaded = 0 for video in videos: youtube_id = video['youtube_id'] # Only save video logs for videos that we recognize. if youtube_id not in ID2SLUG_MAP: logging.warn("Skipping unknown video %s" % youtube_id) continue try: (vl, _) = VideoLog.get_or_initialize(user=user, youtube_id=video["youtube_id"]) for key, val in video.iteritems(): setattr(vl, key, val) logging.debug("Saving video log for %s: %s" % (youtube_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: %s" % e return JsonResponse({"error": error_message}, status=500) # 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: %s" % e return JsonResponse({"error": error_message}, status=500) return JsonResponse({ "success": "Uploaded %d exercises and %d videos" % (n_exercises_uploaded, n_videos_uploaded) })
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") # Save videos video_logs = [] for video in videos: youtube_id = video.get('video', {}).get('youtube_id', "") # Only save videos with progress if not video.get('seconds_watched', None): continue # Only save video logs for videos that we recognize. if youtube_id not in ID2SLUG_MAP: logging.warn("Skipping unknown video %s" % youtube_id) continue try: video_logs.append({ "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" % (youtube_id, video_logs[-1])) except KeyError: # logging.error( "Could not save video log for data with missing values: %s" % video) # Save 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]['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 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)) message = json.loads(response.content) # If something broke on the distribute d server, we are SCREWED. # 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( request.session["distributed_redirect_url"] + "?message_type=%s&message=%s&message_id=id_khanload" % (message.keys()[0], message.values()[0]))
def compute_total_points(user): if user.is_teacher: return None else: return VideoLog.get_points_for_user(user) + ExerciseLog.get_points_for_user(user)
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) video_id = youtube_id = video.get('video', {}).get('youtube_id', "") # 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 distribute d server, we are SCREWED. # 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", }))
class TestSaveExerciseLog(KALiteTestCase): ORIGINAL_POINTS = 37 ORIGINAL_ATTEMPTS = 3 ORIGINAL_STREAK_PROGRESS = 20 NEW_POINTS_LARGER = 22 NEW_ATTEMPTS = 5 NEW_STREAK_PROGRESS_LARGER = 10 NEW_POINTS_SMALLER = 0 NEW_STREAK_PROGRESS_SMALLER = 0 EXERCISE_ID = "number_line" EXERCISE_ID2 = "radius_diameter_and_circumference" USERNAME = "******" PASSWORD = "******" def setUp(self): super(TestSaveExerciseLog, self).setUp() # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username=self.USERNAME, facility=self.facility) self.user.set_password(self.PASSWORD) self.user.save() # create an initial ExerciseLog instance so we have something to update later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.streak_progress = self.ORIGINAL_STREAK_PROGRESS self.original_exerciselog.save() def test_new_exerciselog(self): # make sure the target exercise log does not already exist exerciselogs = ExerciseLog.objects.filter(exercise_id=self.EXERCISE_ID2, user__username=self.USERNAME) self.assertEqual(exerciselogs.count(), 0, "The target exercise log to be newly created already exists") c = KALiteClient() # login success = c.login(username=self.USERNAME, password=self.PASSWORD, facility=self.facility.id) self.assertTrue(success, "Was not able to login as the test user") # save a new exercise log result = c.save_exercise_log( exercise_id=self.EXERCISE_ID2, streak_progress=self.NEW_STREAK_PROGRESS_LARGER, points=self.NEW_POINTS_LARGER, correct=True, attempts=self.NEW_ATTEMPTS, ) self.assertEqual(result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the newly created ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID2, user__username=self.USERNAME) # make sure the ExerciseLog was properly created self.assertEqual(exerciselog.points, self.NEW_POINTS_LARGER, "The ExerciseLog's points were not saved correctly.") self.assertEqual(exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_LARGER, "The ExerciseLog's streak progress was not saved correctly.") self.assertEqual(exerciselog.attempts, self.NEW_ATTEMPTS, "The ExerciseLog did not have the correct number of attempts (%d)." % self.NEW_ATTEMPTS) def test_update_exerciselog(self): # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog hasn't already been changed self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.streak_progress, self.ORIGINAL_STREAK_PROGRESS, "The ExerciseLog's streak progress already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.") c = KALiteClient() # login success = c.login(username=self.USERNAME, password=self.PASSWORD, facility=self.facility.id) self.assertTrue(success, "Was not able to login as the test user") # save a new record onto the exercise log, with a correct answer (increasing the points and streak) result = c.save_exercise_log( exercise_id=self.EXERCISE_ID, streak_progress=self.NEW_STREAK_PROGRESS_LARGER, points=self.NEW_POINTS_LARGER, correct=True, attempts=self.NEW_ATTEMPTS, ) self.assertEqual(result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the updated ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID, user__username=self.USERNAME) # make sure the ExerciseLog was properly updated self.assertEqual(exerciselog.points, self.NEW_POINTS_LARGER, "The ExerciseLog's points were not updated correctly.") self.assertEqual(exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_LARGER, "The ExerciseLog's streak progress was not updated correctly.") self.assertEqual(exerciselog.attempts, self.NEW_ATTEMPTS, "The ExerciseLog did not have the correct number of attempts (%d)." % self.NEW_ATTEMPTS) # save a new record onto the exercise log, with an incorrect answer (decreasing the points and streak) result = c.save_exercise_log( exercise_id=self.EXERCISE_ID, streak_progress=self.NEW_STREAK_PROGRESS_SMALLER, points=self.NEW_POINTS_SMALLER, correct=False, attempts=self.NEW_ATTEMPTS + 1, ) self.assertEqual(result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the updated ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID, user__username=self.USERNAME) # make sure the ExerciseLog was properly updated self.assertEqual(exerciselog.points, self.NEW_POINTS_SMALLER, "The ExerciseLog's points were not saved correctly.") self.assertEqual(exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_SMALLER, "The ExerciseLog's streak progress was not saved correctly.") self.assertEqual(exerciselog.attempts, self.NEW_ATTEMPTS + 1, "The ExerciseLog did not have the correct number of attempts.")
class TestSaveExerciseLog(KALiteTestCase): ORIGINAL_POINTS = 37 ORIGINAL_ATTEMPTS = 3 ORIGINAL_STREAK_PROGRESS = 20 NEW_POINTS_LARGER = 22 NEW_ATTEMPTS = 5 NEW_STREAK_PROGRESS_LARGER = 10 NEW_POINTS_SMALLER = 0 NEW_STREAK_PROGRESS_SMALLER = 0 EXERCISE_ID = "number_line" EXERCISE_ID2 = "radius_diameter_and_circumference" USERNAME = "******" PASSWORD = "******" def setUp(self): super(TestSaveExerciseLog, self).setUp() # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username=self.USERNAME, facility=self.facility) self.user.set_password(self.PASSWORD) self.user.save() # create an initial ExerciseLog instance so we have something to update later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.streak_progress = self.ORIGINAL_STREAK_PROGRESS self.original_exerciselog.save() def test_new_exerciselog(self): # make sure the target exercise log does not already exist exerciselogs = ExerciseLog.objects.filter( exercise_id=self.EXERCISE_ID2, user__username=self.USERNAME) self.assertEqual( exerciselogs.count(), 0, "The target exercise log to be newly created already exists") c = KALiteClient() # login success = c.login(username=self.USERNAME, password=self.PASSWORD, facility=self.facility.id) self.assertTrue(success, "Was not able to login as the test user") # save a new exercise log result = c.save_exercise_log( exercise_id=self.EXERCISE_ID2, streak_progress=self.NEW_STREAK_PROGRESS_LARGER, points=self.NEW_POINTS_LARGER, correct=True, attempts=self.NEW_ATTEMPTS, ) self.assertEqual( result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the newly created ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID2, user__username=self.USERNAME) # make sure the ExerciseLog was properly created self.assertEqual(exerciselog.points, self.NEW_POINTS_LARGER, "The ExerciseLog's points were not saved correctly.") self.assertEqual( exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_LARGER, "The ExerciseLog's streak progress was not saved correctly.") self.assertEqual( exerciselog.attempts, self.NEW_ATTEMPTS, "The ExerciseLog did not have the correct number of attempts (%d)." % self.NEW_ATTEMPTS) def test_update_exerciselog(self): # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog hasn't already been changed self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.streak_progress, self.ORIGINAL_STREAK_PROGRESS, "The ExerciseLog's streak progress already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.") c = KALiteClient() # login success = c.login(username=self.USERNAME, password=self.PASSWORD, facility=self.facility.id) self.assertTrue(success, "Was not able to login as the test user") # save a new record onto the exercise log, with a correct answer (increasing the points and streak) result = c.save_exercise_log( exercise_id=self.EXERCISE_ID, streak_progress=self.NEW_STREAK_PROGRESS_LARGER, points=self.NEW_POINTS_LARGER, correct=True, attempts=self.NEW_ATTEMPTS, ) self.assertEqual( result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the updated ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID, user__username=self.USERNAME) # make sure the ExerciseLog was properly updated self.assertEqual( exerciselog.points, self.NEW_POINTS_LARGER, "The ExerciseLog's points were not updated correctly.") self.assertEqual( exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_LARGER, "The ExerciseLog's streak progress was not updated correctly.") self.assertEqual( exerciselog.attempts, self.NEW_ATTEMPTS, "The ExerciseLog did not have the correct number of attempts (%d)." % self.NEW_ATTEMPTS) # save a new record onto the exercise log, with an incorrect answer (decreasing the points and streak) result = c.save_exercise_log( exercise_id=self.EXERCISE_ID, streak_progress=self.NEW_STREAK_PROGRESS_SMALLER, points=self.NEW_POINTS_SMALLER, correct=False, attempts=self.NEW_ATTEMPTS + 1, ) self.assertEqual( result.status_code, 200, "An error (%d) was thrown while saving the exercise log." % result.status_code) # get a reference to the updated ExerciseLog exerciselog = ExerciseLog.objects.get(exercise_id=self.EXERCISE_ID, user__username=self.USERNAME) # make sure the ExerciseLog was properly updated self.assertEqual(exerciselog.points, self.NEW_POINTS_SMALLER, "The ExerciseLog's points were not saved correctly.") self.assertEqual( exerciselog.streak_progress, self.NEW_STREAK_PROGRESS_SMALLER, "The ExerciseLog's streak progress was not saved correctly.") self.assertEqual( exerciselog.attempts, self.NEW_ATTEMPTS + 1, "The ExerciseLog did not have the correct number of attempts.")
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) video_id = youtube_id = video.get('video', {}).get('youtube_id', "") # 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 distribute d server, we are SCREWED. # 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", }))
class TestExerciseLogs(KALiteTestCase): ORIGINAL_POINTS = 37 ORIGINAL_ATTEMPTS = 3 NEW_POINTS = 22 NEW_ATTEMPTS = 5 EXERCISE_ID = "number_line" def setUp(self): super(TestExerciseLogs, self).setUp() # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username="******", facility=self.facility) self.user.set_password("dumber") self.user.save() # create an initial ExerciseLog instance so we have something to collide with later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was saved as intended self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.") def test_exerciselog_update(self): # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # update the ExerciseLog exerciselog.points = self.NEW_POINTS exerciselog.attempts = self.NEW_ATTEMPTS exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog2 = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was updated self.assertEqual(exerciselog2.points, self.NEW_POINTS, "The ExerciseLog's points were not updated.") self.assertEqual(exerciselog2.attempts, self.NEW_ATTEMPTS, "The ExerciseLog's attempts were not updated.") @unittest.skip("Auto-merging is not yet automatic, so skip this") def test_exerciselog_collision(self): # create a new exercise log with the same exercise_id and user, but different points/attempts exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) exerciselog.points = self.NEW_POINTS exerciselog.attempts = self.NEW_ATTEMPTS # try saving the new ExerciseLog: this is where the collision will happen, hopefully leading to a merge exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog2 = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog has been properly merged self.assertEqual(exerciselog.points, max(self.ORIGINAL_POINTS, self.NEW_POINTS), "The ExerciseLog's points were not properly merged.") self.assertEqual(exerciselog.attempts, max(self.ORIGINAL_ATTEMPTS, self.NEW_ATTEMPTS), "The ExerciseLog's attempts have already changed.")
def generate_fake_exercise_logs(facility_user=None, topics=topics, start_date=datetime.datetime.now() - datetime.timedelta(days=30 * 6)): """Add exercise logs for the given topics, for each of the given users. If no users are given, they are created. If no topics exist, they are taken from the list at the top of this file. By default, users start learning randomly between 6 months ago and now. """ date_diff = datetime.datetime.now() - start_date exercise_logs = [] user_logs = [] # It's not a user: probably a list. # Recursive case if not hasattr(facility_user, "username"): # It's NONE :-/ generate the users first! if not facility_user: (facility_user, _, _) = generate_fake_facility_users() for topic in topics: for user in facility_user: (elogs, ulogs) = generate_fake_exercise_logs(facility_user=user, topics=[topic], start_date=start_date) exercise_logs.append(elogs) user_logs.append(ulogs) # Actually generate! else: # Get (or create) user type try: user_settings = json.loads(facility_user.notes) except: user_settings = sample_user_settings() facility_user.notes = json.dumps(user_settings) facility_user.save() date_diff_started = datetime.timedelta(seconds=datediff(date_diff, units="seconds") * user_settings["time_in_program"]) # when this user started in the program, relative to NOW for topic in topics: # Get all exercises related to the topic exercises = get_topic_exercises(topic_id=topic) # Problem: # Not realistic for students to have lots of unfinished exercises. # If they start them, they tend to get stuck, right? # # So, need to make it more probable that they will finish an exercise, # and less probable that they start one. # # What we need is P(streak|started), not P(streak) # Probability of doing any particular exercise p_exercise = probability_of(qty="exercise", user_settings=user_settings) logging.debug("# exercises: %d; p(exercise)=%4.3f, user settings: %s\n" % (len(exercises), p_exercise, json.dumps(user_settings))) # of exercises is related to for j, exercise in enumerate(exercises): if random.random() > p_exercise: continue # Probability of completing this exercise, and .. proportion of attempts p_completed = probability_of(qty="completed", user_settings=user_settings) p_attempts = probability_of(qty="attempts", user_settings=user_settings) attempts = int(random.random() * p_attempts * 30 + 10) # always enough to have completed completed = (random.random() < p_completed) if completed: streak_progress = 100 else: streak_progress = max(0, min(90, random.gauss(100 * user_settings["speed_of_learning"], 20))) streak_progress = int(floor(streak_progress / 10.)) * 10 points = streak_progress / 10 * 12 if completed else 0 # only get points when you master. # Choose a rate of exercises, based on their effort level and speed of learning. # Compute the latest possible start time. # Then sample a start time between their start time # and the latest possible start_time rate_of_exercises = 0.66 * user_settings["effort_level"] + 0.33 * user_settings["speed_of_learning"] # exercises per day time_for_attempts = min(datetime.timedelta(days=rate_of_exercises * attempts), date_diff_started) # protect with min time_delta_completed = datetime.timedelta(seconds=random.randint(int(datediff(time_for_attempts, units="seconds")), int(datediff(date_diff_started, units="seconds")))) date_completed = datetime.datetime.now() - time_delta_completed # Always create new logging.info("Creating exercise log: %-12s: %-25s (%d points, %d attempts, %d%% streak on %s)" % ( facility_user.first_name, exercise["name"], points, attempts, streak_progress, date_completed, )) try: elog = ExerciseLog.objects.get(user=facility_user, exercise_id=exercise["name"]) except ExerciseLog.DoesNotExist: elog = ExerciseLog( user=facility_user, exercise_id=exercise["name"], attempts=int(attempts), streak_progress=streak_progress, points=int(points), completion_timestamp=date_completed, completion_counter=datediff(date_completed, start_date, units="seconds"), ) elog.full_clean() elog.save() # TODO(bcipolli): bulk saving of logs # For now, make all attempts on an exercise into a single UserLog. seconds_per_attempt = 10 * (1 + user_settings["speed_of_learning"] * random.random()) time_to_navigate = 15 * (0.5 + random.random()) #between 7.5s and 22.5s time_to_logout = 5 * (0.5 + random.random()) # between 2.5 and 7.5s ulog = UserLog( user=facility_user, activity_type=1, start_datetime = date_completed - datetime.timedelta(seconds=int(attempts * seconds_per_attempt + time_to_navigate)), end_datetime = date_completed + datetime.timedelta(seconds=time_to_logout), last_active_datetime = date_completed, ) ulog.full_clean() ulog.save() user_logs.append(ulog) exercise_logs.append(elog) return (exercise_logs, user_logs)
def generate_fake_exercise_logs(facility_user=None, topics=topics, start_date=datetime.datetime.now() - datetime.timedelta(days=30 * 6)): """Add exercise logs for the given topics, for each of the given users. If no users are given, they are created. If no topics exist, they are taken from the list at the top of this file. By default, users start learning randomly between 6 months ago and now. """ own_device = Device.get_own_device() date_diff = datetime.datetime.now() - start_date exercise_logs = [] user_logs = [] # It's not a user: probably a list. # Recursive case if not hasattr(facility_user, "username"): # It's NONE :-/ generate the users first! if not facility_user: (facility_user, _, _) = generate_fake_facility_users() for topic in topics: for user in facility_user: (elogs, ulogs) = generate_fake_exercise_logs(facility_user=user, topics=[topic], start_date=start_date) exercise_logs.append(elogs) user_logs.append(ulogs) # Actually generate! else: # Get (or create) user type try: user_settings = json.loads(facility_user.notes) except: user_settings = sample_user_settings() facility_user.notes = json.dumps(user_settings) facility_user.save() date_diff_started = datetime.timedelta(seconds=datediff(date_diff, units="seconds") * user_settings["time_in_program"]) # when this user started in the program, relative to NOW for topic in topics: # Get all exercises related to the topic exercises = get_topic_exercises(topic_id=topic) # Problem: # Not realistic for students to have lots of unfinished exercises. # If they start them, they tend to get stuck, right? # # So, need to make it more probable that they will finish an exercise, # and less probable that they start one. # # What we need is P(streak|started), not P(streak) # Probability of doing any particular exercise p_exercise = probability_of(qty="exercise", user_settings=user_settings) logging.debug("# exercises: %d; p(exercise)=%4.3f, user settings: %s\n" % (len(exercises), p_exercise, json.dumps(user_settings))) # of exercises is related to for j, exercise in enumerate(exercises): if random.random() > p_exercise: continue # Probability of completing this exercise, and .. proportion of attempts p_completed = probability_of(qty="completed", user_settings=user_settings) p_attempts = probability_of(qty="attempts", user_settings=user_settings) attempts = int(random.random() * p_attempts * 30 + 10) # always enough to have completed completed = (random.random() < p_completed) if completed: streak_progress = 100 else: streak_progress = max(0, min(90, random.gauss(100 * user_settings["speed_of_learning"], 20))) streak_progress = int(floor(streak_progress / 10.)) * 10 points = streak_progress / 10 * 12 if completed else 0 # only get points when you master. # Choose a rate of exercises, based on their effort level and speed of learning. # Compute the latest possible start time. # Then sample a start time between their start time # and the latest possible start_time rate_of_exercises = 0.66 * user_settings["effort_level"] + 0.33 * user_settings["speed_of_learning"] # exercises per day time_for_attempts = min(datetime.timedelta(days=rate_of_exercises * attempts), date_diff_started) # protect with min time_delta_completed = datetime.timedelta(seconds=random.randint(int(datediff(time_for_attempts, units="seconds")), int(datediff(date_diff_started, units="seconds")))) date_completed = datetime.datetime.now() - time_delta_completed # Always create new logging.info("Creating exercise log: %-12s: %-25s (%d points, %d attempts, %d%% streak on %s)" % ( facility_user.first_name, exercise["name"], points, attempts, streak_progress, date_completed, )) try: elog = ExerciseLog.objects.get(user=facility_user, exercise_id=exercise["name"]) except ExerciseLog.DoesNotExist: elog = ExerciseLog( user=facility_user, exercise_id=exercise["name"], attempts=int(attempts), streak_progress=streak_progress, points=int(points), complete=completed, completion_timestamp=date_completed, completion_counter=datediff(date_completed, start_date, units="seconds"), ) elog.counter = own_device.increment_and_get_counter() elog.sign(own_device) # have to sign after setting the counter elog.save(imported=True) # avoid userlog issues # For now, make all attempts on an exercise into a single UserLog. seconds_per_attempt = 10 * (1 + user_settings["speed_of_learning"] * random.random()) time_to_navigate = 15 * (0.5 + random.random()) #between 7.5s and 22.5s time_to_logout = 5 * (0.5 + random.random()) # between 2.5 and 7.5s if settings.USER_LOG_MAX_RECORDS_PER_USER != 0: ulog = UserLog( user=facility_user, activity_type=1, start_datetime = date_completed - datetime.timedelta(seconds=int(attempts * seconds_per_attempt + time_to_navigate)), end_datetime = date_completed + datetime.timedelta(seconds=time_to_logout), last_active_datetime = date_completed, ) ulog.full_clean() ulog.save() user_logs.append(ulog) exercise_logs.append(elog) return (exercise_logs, user_logs)
class TestExerciseLogs(TestCase): ORIGINAL_POINTS = 37 ORIGINAL_ATTEMPTS = 3 NEW_POINTS = 22 NEW_ATTEMPTS = 5 EXERCISE_ID = "number_line" def setUp(self): # create a facility and user that can be referred to in models across tests self.facility = Facility(name="Test Facility") self.facility.save() self.user = FacilityUser(username="******", password="******", facility=self.facility) self.user.save() # create an initial ExerciseLog instance so we have something to collide with later self.original_exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) self.original_exerciselog.points = self.ORIGINAL_POINTS self.original_exerciselog.attempts = self.ORIGINAL_ATTEMPTS self.original_exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was saved as intended self.assertEqual(exerciselog.points, self.ORIGINAL_POINTS, "The ExerciseLog's points have already changed.") self.assertEqual(exerciselog.attempts, self.ORIGINAL_ATTEMPTS, "The ExerciseLog's attempts have already changed.") def test_exerciselog_update(self): # get a new reference to the existing ExerciseLog exerciselog = ExerciseLog.objects.get(id=self.original_exerciselog.id) # update the ExerciseLog exerciselog.points = self.NEW_POINTS exerciselog.attempts = self.NEW_ATTEMPTS exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog2 = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog was updated self.assertEqual(exerciselog2.points, self.NEW_POINTS, "The ExerciseLog's points were not updated.") self.assertEqual(exerciselog2.attempts, self.NEW_ATTEMPTS, "The ExerciseLog's attempts were not updated.") @unittest.skip("Auto-merging is not yet automatic, so skip this") def test_exerciselog_collision(self): # create a new exercise log with the same exercise_id and user, but different points/attempts exerciselog = ExerciseLog(exercise_id=self.EXERCISE_ID, user=self.user) exerciselog.points = self.NEW_POINTS exerciselog.attempts = self.NEW_ATTEMPTS # try saving the new ExerciseLog: this is where the collision will happen, hopefully leading to a merge exerciselog.save() # get a new reference to the existing ExerciseLog exerciselog2 = ExerciseLog.objects.get(id=self.original_exerciselog.id) # make sure the ExerciseLog has been properly merged self.assertEqual(exerciselog.points, max(self.ORIGINAL_POINTS, self.NEW_POINTS), "The ExerciseLog's points were not properly merged.") self.assertEqual(exerciselog.attempts, max(self.ORIGINAL_ATTEMPTS, self.NEW_ATTEMPTS), "The ExerciseLog's attempts have already changed.")
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") # Save videos video_logs = [] for video in videos: youtube_id =video.get('video', {}).get('youtube_id', "") # Only save videos with progress if not video.get('seconds_watched', None): continue # Only save video logs for videos that we recognize. if youtube_id not in ID2SLUG_MAP: logging.warn("Skipping unknown video %s" % youtube_id) continue try: video_logs.append({ "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" % (youtube_id, video_logs[-1])) except KeyError: # logging.error("Could not save video log for data with missing values: %s" % video) # Save 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]['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 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)) message = json.loads(response.content) # If something broke on the distribute d server, we are SCREWED. # 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(request.session["distributed_redirect_url"] + "?message_type=%s&message=%s&message_id=id_khanload" % (message.keys()[0], message.values()[0]))