From e4312d67f0934f999057fe564861cd8df4af2a8c Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 19 Feb 2013 09:43:19 -0500 Subject: [PATCH 01/10] work in progress on supporting science puzzles --- lms/djangoapps/foldit/models.py | 30 +++++++++++ lms/djangoapps/foldit/tests.py | 95 ++++++++++++++++++++++----------- 2 files changed, 93 insertions(+), 32 deletions(-) diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index ea4f099216..6ea180ff2c 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -25,6 +25,36 @@ class Score(models.Model): score_version = models.IntegerField() created = models.DateTimeField(auto_now_add=True) + @staticmethod + def display_score(score): + """ + Argument: + score (float), as stored in the DB + + Returns: + score (float), as displayed to the user in the game and in the leaderboard + """ + # TODO: put in correct formula + return -score + + @staticmethod + def get_top_n(puzzle_id, n): + """ + Arguments: + puzzle_id (int): id of the puzzle for which to look + n (int): number of top scores to return. + + Returns: + The top (lowest energy, highest display score) n scores for the puzzle. If + there are fewer than n, returns all. Output is a list of dictionaries, sorted + by display_score: + [ {username: 'a_user', + score: 8500}, ...] + """ + scores = Score.objects.filter(puzzle_id=puzzle_id).order_by('-best_score')[:n] + return [{'username': s.user.username, 'score': display_score(s.best_score)} + for s in scores] + class PuzzleComplete(models.Model): """ diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 4d387b44e8..d560416f4b 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -9,7 +9,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from foldit.views import foldit_ops, verify_code -from foldit.models import PuzzleComplete +from foldit.models import PuzzleComplete, Score from student.models import UserProfile, unique_id_for_user from datetime import datetime, timedelta @@ -37,13 +37,19 @@ class FolditTestCase(TestCase): request.user = self.user return request - def test_SetPlayerPuzzleScores(self): - - scores = [ {"PuzzleID": 994391, + def make_puzzle_score_request(self, puzzle_ids, best_scores): + """ + Given lists of puzzle_ids and best_scores (must have same length), make a + SetPlayerPuzzleScores request and return the response. + """ + def score_dict(puzzle_id, best_score): + return {"PuzzleID": puzzle_id, "ScoreType": "score", - "BestScore": 0.078034, - "CurrentScore":0.080035, - "ScoreVersion":23}] + "BestScore": best_score, + # current scores don't actually matter + "CurrentScore": best_score + 0.01, + "ScoreVersion": 23} + scores = [score_dict(pid, bs) for pid, bs in zip(puzzle_ids, best_scores)] scores_str = json.dumps(scores) verify = {"Verify": verify_code(self.user.email, scores_str), @@ -55,51 +61,76 @@ class FolditTestCase(TestCase): response = foldit_ops(request) self.assertEqual(response.status_code, 200) + return response + + def test_SetPlayerPuzzleScores(self): + + puzzle_id = 994391 + best_score = 0.078034 + response = self.make_puzzle_score_request([puzzle_id], [best_score]) self.assertEqual(response.content, json.dumps( [{"OperationID": "SetPlayerPuzzleScores", "Value": [{ - "PuzzleID": 994391, + "PuzzleID": puzzle_id, "Status": "Success"}]}])) + # There should now be a score in the db. + top_10 = Score.get_top_n(puzzle_id, 10) + self.assertEqual(len(top_10), 1) + self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) + def test_SetPlayerPuzzleScores_many(self): - scores = [ {"PuzzleID": 994391, - "ScoreType": "score", - "BestScore": 0.078034, - "CurrentScore":0.080035, - "ScoreVersion":23}, - {"PuzzleID": 994392, - "ScoreType": "score", - "BestScore": 0.078000, - "CurrentScore":0.080011, - "ScoreVersion":23}] - - scores_str = json.dumps(scores) - - verify = {"Verify": verify_code(self.user.email, scores_str), - "VerifyMethod":"FoldItVerify"} - data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), - 'SetPlayerPuzzleScores': scores_str} - - request = self.make_request(data) - - response = foldit_ops(request) - self.assertEqual(response.status_code, 200) + response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000]) self.assertEqual(response.content, json.dumps( [{"OperationID": "SetPlayerPuzzleScores", "Value": [{ - "PuzzleID": 994391, + "PuzzleID": 1, "Status": "Success"}, - {"PuzzleID": 994392, + {"PuzzleID": 2, "Status": "Success"}]}])) + + def test_SetPlayerPuzzleScores_multiple(self): + """ + Check that multiple posts with the same id are handled properly + (keep latest for each user, have multiple users work properly) + """ + orig_score = 0.07 + puzzle_id = 1 + response = self.make_puzzle_score_request([puzzle_id], [orig_score]) + + # There should now be a score in the db. + top_10 = Score.get_top_n(puzzle_id, 10) + self.assertEqual(len(top_10), 1) + self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) + + # Reporting a better score should overwrite + better_score = 0.06 + response = self.make_puzzle_score_request([1], [better_score]) + + top_10 = Score.get_top_n(puzzle_id, 10) + self.assertEqual(len(top_10), 1) + self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) + + # reporting a worse score shouldn't + worse_score = 0.065 + response = self.make_puzzle_score_request([1], [worse_score]) + + top_10 = Score.get_top_n(puzzle_id, 10) + self.assertEqual(len(top_10), 1) + # should still be the better score + self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) + + + def test_SetPlayerPuzzleScores_error(self): scores = [ {"PuzzleID": 994391, From e5294591dc5f35c5e42406b26509f7b6a6b54c0c Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Fri, 22 Feb 2013 11:12:26 -0500 Subject: [PATCH 02/10] Foldit leaderboard w/o tests --- common/lib/xmodule/xmodule/foldit_module.py | 10 ++++++ lms/djangoapps/foldit/models.py | 36 ++++++++++++++++++--- lms/djangoapps/foldit/views.py | 21 ++++++++++++ lms/templates/foldit.html | 19 ++++++++++- 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index ea16fee7f1..bfe921a068 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -66,6 +66,14 @@ class FolditModule(XModule): PuzzleComplete.completed_puzzles(self.system.anonymous_student_id), key=lambda d: (d['set'], d['subset'])) + def puzzle_leaders(self, n=10): + """ + Returns a list of n pairs (user, score) corresponding to the top + scores; the pairs are in descending order of score. + """ + from foldit.models import Score + + return [(e['username'], e['total_score']) for e in Score.get_tops_n(10)] def get_html(self): """ @@ -80,6 +88,7 @@ class FolditModule(XModule): 'success': self.is_complete(), 'goal_level': goal_level, 'completed': self.completed_puzzles(), + 'top_scores': self.puzzle_leaders(), } return self.system.render_template('foldit.html', context) @@ -97,6 +106,7 @@ class FolditModule(XModule): return 1 + class FolditDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding open ended response questions to courses diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index 6ea180ff2c..3202402e52 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -26,17 +26,19 @@ class Score(models.Model): created = models.DateTimeField(auto_now_add=True) @staticmethod - def display_score(score): + def display_score(score, sum_of=1): """ Argument: - score (float), as stored in the DB + score (float), as stored in the DB (i.e., "rosetta score") + sum_of (int): if this score is the sum of scores of individual + problems, how many elements are in that sum Returns: score (float), as displayed to the user in the game and in the leaderboard """ - # TODO: put in correct formula - return -score + return (-score) * 10 + 8000 * sum_of + # TODO: delete this, incorporate it in get_tops_n @staticmethod def get_top_n(puzzle_id, n): """ @@ -52,9 +54,33 @@ class Score(models.Model): score: 8500}, ...] """ scores = Score.objects.filter(puzzle_id=puzzle_id).order_by('-best_score')[:n] - return [{'username': s.user.username, 'score': display_score(s.best_score)} + return [{'username': s.user.username, 'score': Score.display_score(s.best_score)} for s in scores] + @staticmethod + def get_tops_n(n, puzzles=['994559']): + """ + Arguments: + puzzles: a list of puzzle ids that we will use. If not specified, + defaults to puzzle used in 7012x. + n (int): number of top scores to return + + + Returns: + The top n sum of scores for puzzles in . Output is a list + of disctionaries, sorted by display_score: + [ {username: 'a_user', + score: 12000} ...] + """ + scores = Score.objects.filter(puzzle_id__in=puzzles).annotate( + total_score=models.Sum('best_score')).order_by( + '-total_score')[:n] + num = len(puzzles) + + return [{'username': s.user.username, + 'total_score': Score.display_score(s.total_score, num)} + for s in scores] + class PuzzleComplete(models.Model): """ diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index 8bd3684c04..8b284704d6 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -10,6 +10,8 @@ from django.views.decorators.csrf import csrf_exempt from foldit.models import Score, PuzzleComplete from student.models import unique_id_for_user +import re + log = logging.getLogger(__name__) @@ -38,6 +40,13 @@ def foldit_ops(request): "user %s, scores json %r, verify %r", request.user, puzzle_scores_json, pz_verify_json) else: + # This is needed because we are not getting valid json - the + # value of ScoreType is an unquoted string. Right now regexes are + # quoting the string, but ideally the json itself would be fixed. + # To allow for fixes without breaking this, the regex should only + # match unquoted strings, + a = re.compile(r':([a-zA-Z]*),') + puzzle_scores_json = re.sub(a, ':"\g<1>",', puzzle_scores_json) puzzle_scores = json.loads(puzzle_scores_json) responses.append(save_scores(request.user, puzzle_scores)) @@ -98,10 +107,22 @@ def save_scores(user, puzzle_scores): # BestScore (energy), CurrentScore (Energy), ScoreVersion (int) puzzle_id = score['PuzzleID'] + best_score = score['BestScore'] + current_score = score['CurrentScore'] + score_version = score['ScoreVersion'] # TODO: save the score # SetPlayerPuzzleScoreResponse object + Score.objects.get_or_create( + user=user, + unique_user_id=unique_id_for_user(user), + puzzle_id=puzzle_id, + best_score=best_score, + current_score=current_score, + score_version=score_version) + + # TODO: get info from db instead? score_responses.append({'PuzzleID': puzzle_id, 'Status': 'Success'}) diff --git a/lms/templates/foldit.html b/lms/templates/foldit.html index 2c16ebbfeb..2460e25f8e 100644 --- a/lms/templates/foldit.html +++ b/lms/templates/foldit.html @@ -25,4 +25,21 @@ You have not yet gotten to level ${goal_level}. % endfor - \ No newline at end of file +
+ +

Puzzle Leaderboard

+ + + + + + + % for pair in top_scores: + + + + + % endfor +
UserScore
${pair[0]}${pair[1]}
+ + From da2d0ed6ec25732dc180f31220ca8af39252c7cf Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 11:16:37 -0500 Subject: [PATCH 03/10] Foldit with puzzle leaderboard --- common/lib/xmodule/xmodule/foldit_module.py | 2 +- lms/djangoapps/foldit/models.py | 41 ++++++------- lms/djangoapps/foldit/tests.py | 64 +++++++++++++++------ lms/djangoapps/foldit/views.py | 28 ++++++--- 4 files changed, 86 insertions(+), 49 deletions(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index bfe921a068..b80f54a41c 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -73,7 +73,7 @@ class FolditModule(XModule): """ from foldit.models import Score - return [(e['username'], e['total_score']) for e in Score.get_tops_n(10)] + return [(e['username'], e['score']) for e in Score.get_tops_n(10)] def get_html(self): """ diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index 3202402e52..df1be3e87c 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -39,23 +39,23 @@ class Score(models.Model): return (-score) * 10 + 8000 * sum_of # TODO: delete this, incorporate it in get_tops_n - @staticmethod - def get_top_n(puzzle_id, n): - """ - Arguments: - puzzle_id (int): id of the puzzle for which to look - n (int): number of top scores to return. + #@staticmethod + #def get_top_n(puzzle_id, n): + #""" + #Arguments: + #puzzle_id (int): id of the puzzle for which to look + #n (int): number of top scores to return. - Returns: - The top (lowest energy, highest display score) n scores for the puzzle. If - there are fewer than n, returns all. Output is a list of dictionaries, sorted - by display_score: - [ {username: 'a_user', - score: 8500}, ...] - """ - scores = Score.objects.filter(puzzle_id=puzzle_id).order_by('-best_score')[:n] - return [{'username': s.user.username, 'score': Score.display_score(s.best_score)} - for s in scores] + #Returns: + #The top (lowest energy, highest display score) n scores for the puzzle. If + #there are fewer than n, returns all. Output is a list of dictionaries, sorted + #by display_score: + #[ {username: 'a_user', + #score: 8500}, ...] + #""" + #scores = Score.objects.filter(puzzle_id=puzzle_id).order_by('-best_score')[:n] + #return [{'username': s.user.username, 'score': Score.display_score(s.best_score)} + #for s in scores] @staticmethod def get_tops_n(n, puzzles=['994559']): @@ -72,13 +72,14 @@ class Score(models.Model): [ {username: 'a_user', score: 12000} ...] """ - scores = Score.objects.filter(puzzle_id__in=puzzles).annotate( - total_score=models.Sum('best_score')).order_by( - '-total_score')[:n] + scores = Score.objects \ + .filter(puzzle_id__in=puzzles) \ + .annotate(total_score=models.Sum('best_score')) \ + .order_by('-total_score')[:n] num = len(puzzles) return [{'username': s.user.username, - 'total_score': Score.display_score(s.total_score, num)} + 'score': Score.display_score(s.total_score, num)} for s in scores] diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index d560416f4b..b81119d614 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -25,19 +25,22 @@ class FolditTestCase(TestCase): pwd = 'abc' self.user = User.objects.create_user('testuser', 'test@test.com', pwd) + self.user2 = User.objects.create_user('testuser2', 'test2@test.com', pwd) self.unique_user_id = unique_id_for_user(self.user) + self.unique_user_id2 = unique_id_for_user(self.user2) now = datetime.now() self.tomorrow = now + timedelta(days=1) self.yesterday = now - timedelta(days=1) UserProfile.objects.create(user=self.user) + UserProfile.objects.create(user=self.user2) - def make_request(self, post_data): + def make_request(self, post_data, user=self.user): request = self.factory.post(self.url, post_data) - request.user = self.user + request.user = user return request - def make_puzzle_score_request(self, puzzle_ids, best_scores): + def make_puzzle_score_request(self, puzzle_ids, best_scores, user=self.user): """ Given lists of puzzle_ids and best_scores (must have same length), make a SetPlayerPuzzleScores request and return the response. @@ -52,8 +55,8 @@ class FolditTestCase(TestCase): scores = [score_dict(pid, bs) for pid, bs in zip(puzzle_ids, best_scores)] scores_str = json.dumps(scores) - verify = {"Verify": verify_code(self.user.email, scores_str), - "VerifyMethod":"FoldItVerify"} + verify = {"Verify": verify_code(user.email, scores_str), + "VerifyMethod": "FoldItVerify"} data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), 'SetPlayerPuzzleScores': scores_str} @@ -65,7 +68,7 @@ class FolditTestCase(TestCase): def test_SetPlayerPuzzleScores(self): - puzzle_id = 994391 + puzzle_id = [994391] best_score = 0.078034 response = self.make_puzzle_score_request([puzzle_id], [best_score]) @@ -76,14 +79,12 @@ class FolditTestCase(TestCase): "Status": "Success"}]}])) # There should now be a score in the db. - top_10 = Score.get_top_n(puzzle_id, 10) + top_10 = Score.get_tops_n(puzzle_id, 10) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) - def test_SetPlayerPuzzleScores_many(self): - response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000]) self.assertEqual(response.content, json.dumps( @@ -96,19 +97,17 @@ class FolditTestCase(TestCase): "Status": "Success"}]}])) - - def test_SetPlayerPuzzleScores_multiple(self): """ Check that multiple posts with the same id are handled properly (keep latest for each user, have multiple users work properly) """ orig_score = 0.07 - puzzle_id = 1 + puzzle_id = ['1'] response = self.make_puzzle_score_request([puzzle_id], [orig_score]) # There should now be a score in the db. - top_10 = Score.get_top_n(puzzle_id, 10) + top_10 = Score.get_tops_n(puzzle_id, 10) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) @@ -116,7 +115,7 @@ class FolditTestCase(TestCase): better_score = 0.06 response = self.make_puzzle_score_request([1], [better_score]) - top_10 = Score.get_top_n(puzzle_id, 10) + top_10 = Score.get_tops_n(puzzle_id, 10) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) @@ -124,24 +123,51 @@ class FolditTestCase(TestCase): worse_score = 0.065 response = self.make_puzzle_score_request([1], [worse_score]) - top_10 = Score.get_top_n(puzzle_id, 10) + top_10 = Score.get_tops_n(puzzle_id, 10) self.assertEqual(len(top_10), 1) # should still be the better score self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) + def test_SetPlayerPyzzleScores_manyplayers(self): + """ + Check that when we send scores from multiple users, the correct order + of scores is displayed. + """ + puzzle_id = ['1'] + player1_score = 0.07 + player2_score = 0.08 + response1 = self.make_puzzle_score_request([puzzle_id], [player1_score], + self.user) + # There should now be a score in the db. + top_10 = Score.get_tops_n(puzzle_id, 10) + self.assertEqual(len(top_10), 1) + self.assertEqual(top_10[0]['score'], Score.display_score(player1_score)) + + response2 = self.make_puzzle_score_request([puzzle_id], [player2_score], + self.user2) + + # There should now be two scores in the db + self.assertEqual(len(top_10), 2) + + # Top score should be player2_score. Second should be player1_score + self.assertEqual(top_10[0]['score'], Score.display_score(player2_score)) + self.assertEqual(top_10[1]['score'], Score.display_score(player1_score)) + + # Top score user should be self.user2.username + self.assertEqual(top_10[0]['username'], self.user2.username) def test_SetPlayerPuzzleScores_error(self): - scores = [ {"PuzzleID": 994391, + scores = [{"PuzzleID": 994391, "ScoreType": "score", "BestScore": 0.078034, - "CurrentScore":0.080035, - "ScoreVersion":23}] + "CurrentScore": 0.080035, + "ScoreVersion": 23}] validation_str = json.dumps(scores) verify = {"Verify": verify_code(self.user.email, validation_str), - "VerifyMethod":"FoldItVerify"} + "VerifyMethod": "FoldItVerify"} # change the real string -- should get an error scores[0]['ScoreVersion'] = 22 diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index 8b284704d6..9939d1aa63 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -111,16 +111,26 @@ def save_scores(user, puzzle_scores): current_score = score['CurrentScore'] score_version = score['ScoreVersion'] - # TODO: save the score - # SetPlayerPuzzleScoreResponse object - Score.objects.get_or_create( - user=user, - unique_user_id=unique_id_for_user(user), - puzzle_id=puzzle_id, - best_score=best_score, - current_score=current_score, - score_version=score_version) + # Score entries are unique on user/unique_user_id/puzzle_id/score_version + try: + obj = Score.objects.get( + user=user, + unique_user_id=unique_id_for_user(user), + puzzle_id=puzzle_id, + score_version=score_version) + obj.current_score = current_score + obj.best_score = best_score + + except Score.DoesNotExist: + obj = Score( + user=user, + unique_user_id=unique_id_for_user(user), + puzzle_id=puzzle_id, + current_score=current_score, + best_score=best_score, + score_version=score_version) + obj.save() # TODO: get info from db instead? score_responses.append({'PuzzleID': puzzle_id, From 26eaf8985c6c9bc5d00a011dfa8050f47f831154 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 17:28:22 -0500 Subject: [PATCH 04/10] Allow progress without leaderboard and vice-versa --- common/lib/xmodule/xmodule/foldit_module.py | 58 ++++++++++++++++++--- lms/templates/foldit.html | 47 +++-------------- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index b80f54a41c..6cbf22980b 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -11,14 +11,27 @@ from xmodule.xml_module import XmlDescriptor log = logging.getLogger(__name__) class FolditModule(XModule): + + css = {'scss': [resource_string(__name__, 'css/foldit/leadeboard.scss')]} + def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) - # ooh look--I'm lazy, so hardcoding the 7.00x required level. - # If we need it generalized, can pull from the xml later - self.required_level = 4 - self.required_sublevel = 5 + """ + + Example: + + """ + req_level = self.metadata.get("required_level") + req_sublevel = self.metadata.get("required_sublevel") + + # default to what Spring_7012x uses + self.required_level = req_level if req_level else 4 + self.required_sublevel = req_sublevel if req_sublevel else 5 def parse_due_date(): """ @@ -83,16 +96,47 @@ class FolditModule(XModule): self.required_level, self.required_sublevel) + showbasic = (self.metadata.get("show_basic_score") == "true") + showleader = (self.metadata.get("show_leaderboard") == "true") context = { 'due': self.due_str, 'success': self.is_complete(), 'goal_level': goal_level, 'completed': self.completed_puzzles(), 'top_scores': self.puzzle_leaders(), + 'show_basic': showbasic, + 'show_leader': showleader, + 'folditbasic': self.get_basicpuzzles_html(), + 'folditchallenge': self.get_challenge_html() } return self.system.render_template('foldit.html', context) + def get_basicpuzzles_html(self): + """ + Render html for the basic puzzle section. + """ + goal_level = '{0}-{1}'.format( + self.required_level, + self.required_sublevel) + + context = { + 'due': self.due_str, + 'success': self.is_complete(), + 'goal_level': goal_level, + 'completed': self.completed_puzzles(), + } + return self.system.render_template('folditbasic.html', context) + + def get_challenge_html(self): + """ + Render html for challenge (i.e., the leaderboard) + """ + + context = { + 'top_scores': self.puzzle_leaders()} + + return self.system.render_template('folditchallenge.html', context) def get_score(self): """ @@ -109,7 +153,7 @@ class FolditModule(XModule): class FolditDescriptor(XmlDescriptor, EditingDescriptor): """ - Module for adding open ended response questions to courses + Module for adding Foldit problems to courses """ mako_template = "widgets/html-edit.html" module_class = FolditModule @@ -129,6 +173,6 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): """ - For now, don't need anything from the xml + Get the xml_object's attributes. """ - return {} + return {'metadata': xml_object.attrib} diff --git a/lms/templates/foldit.html b/lms/templates/foldit.html index 2460e25f8e..2a8271cc62 100644 --- a/lms/templates/foldit.html +++ b/lms/templates/foldit.html @@ -1,45 +1,12 @@
-

Due: ${due} - -

-Status: -% if success: -You have successfully gotten to level ${goal_level}. -% else: -You have not yet gotten to level ${goal_level}. -% endif -

- -

Completed puzzles

- - - - - - - % for puzzle in completed: - - - - - % endfor -
LevelSubmitted
${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}${puzzle['created'].strftime('%Y-%m-%d %H:%M')}
- -
-

Puzzle Leaderboard

+ % if show_basic: + ${folditbasic} + % endif - - - - - - % for pair in top_scores: - - - - - % endfor -
UserScore
${pair[0]}${pair[1]}
+ + % if show_leader: + ${folditchallenge} + % endif
From edba0978cfa302833ae75786dcb53453e3aee5ee Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 18:18:46 -0500 Subject: [PATCH 05/10] Included styling and template --- .../xmodule/css/foldit/leaderboard.scss | 20 +++++++++++++ common/lib/xmodule/xmodule/foldit_module.py | 2 +- lms/templates/folditbasic.html | 29 +++++++++++++++++++ lms/templates/folditchallenge.html | 16 ++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/css/foldit/leaderboard.scss create mode 100644 lms/templates/folditbasic.html create mode 100644 lms/templates/folditchallenge.html diff --git a/common/lib/xmodule/xmodule/css/foldit/leaderboard.scss b/common/lib/xmodule/xmodule/css/foldit/leaderboard.scss new file mode 100644 index 0000000000..5342c985c2 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/foldit/leaderboard.scss @@ -0,0 +1,20 @@ +$leaderboard: #F4F4F4; + +section.foldit { + div.folditchallenge { + table { + border: 1px solid lighten($leaderboard, 10%); + border-collapse: collapse; + margin-top: 20px; + } + th { + background: $leaderboard; + color: darken($leaderboard, 25%); + } + td { + background: lighten($leaderboard, 3%); + border-bottom: 1px solid #fff; + padding: 8px; + } + } +} diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 6cbf22980b..7f46a34b0f 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) class FolditModule(XModule): - css = {'scss': [resource_string(__name__, 'css/foldit/leadeboard.scss')]} + css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]} def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/lms/templates/folditbasic.html b/lms/templates/folditbasic.html new file mode 100644 index 0000000000..0c79a53703 --- /dev/null +++ b/lms/templates/folditbasic.html @@ -0,0 +1,29 @@ +
+

Due: ${due} + +

+ Status: + % if success: + You have successfully gotten to level ${goal_level}. + % else: + You have not yet gotten to level ${goal_level}. + % endif +

+ +

Completed puzzles

+ + + + + + + % for puzzle in completed: + + + + + % endfor +
LevelSubmitted
${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}${puzzle['created'].strftime('%Y-%m-%d %H:%M')}
+ +
+
diff --git a/lms/templates/folditchallenge.html b/lms/templates/folditchallenge.html new file mode 100644 index 0000000000..677bc286c8 --- /dev/null +++ b/lms/templates/folditchallenge.html @@ -0,0 +1,16 @@ +
+

Puzzle Leaderboard

+ + + + + + + % for pair in top_scores: + + + + + % endfor +
UserScore
${pair[0]}${pair[1]}
+
From 3473a5d3b0e15e4cd48974b76bc1bc6bc53e9c63 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 18:39:59 -0500 Subject: [PATCH 06/10] Fixed broken reference --- lms/djangoapps/foldit/tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index b81119d614..3a0fe54503 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -35,16 +35,18 @@ class FolditTestCase(TestCase): UserProfile.objects.create(user=self.user) UserProfile.objects.create(user=self.user2) - def make_request(self, post_data, user=self.user): + def make_request(self, post_data, user=None): request = self.factory.post(self.url, post_data) - request.user = user + request.user = self.user if not user else user return request - def make_puzzle_score_request(self, puzzle_ids, best_scores, user=self.user): + def make_puzzle_score_request(self, puzzle_ids, best_scores, user=None): """ Given lists of puzzle_ids and best_scores (must have same length), make a SetPlayerPuzzleScores request and return the response. """ + user = self.user if not user else user + def score_dict(puzzle_id, best_score): return {"PuzzleID": puzzle_id, "ScoreType": "score", @@ -109,7 +111,7 @@ class FolditTestCase(TestCase): # There should now be a score in the db. top_10 = Score.get_tops_n(puzzle_id, 10) self.assertEqual(len(top_10), 1) - self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) + self.assertEqual(top_10[0]['score'], Score.display_score(orig_score)) # Reporting a better score should overwrite better_score = 0.06 From b4387b287d4d8758f9eb98d2dcee25b30e8e9d2e Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 18:56:58 -0500 Subject: [PATCH 07/10] Adding migrations --- .../foldit/migrations/0001_initial.py | 111 ++++++++++++++++++ lms/djangoapps/foldit/migrations/__init__.py | 0 2 files changed, 111 insertions(+) create mode 100644 lms/djangoapps/foldit/migrations/0001_initial.py create mode 100644 lms/djangoapps/foldit/migrations/__init__.py diff --git a/lms/djangoapps/foldit/migrations/0001_initial.py b/lms/djangoapps/foldit/migrations/0001_initial.py new file mode 100644 index 0000000000..6c9edfeaa4 --- /dev/null +++ b/lms/djangoapps/foldit/migrations/0001_initial.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Score' + db.create_table('foldit_score', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='foldit_scores', to=orm['auth.User'])), + ('unique_user_id', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)), + ('puzzle_id', self.gf('django.db.models.fields.IntegerField')()), + ('best_score', self.gf('django.db.models.fields.FloatField')(db_index=True)), + ('current_score', self.gf('django.db.models.fields.FloatField')(db_index=True)), + ('score_version', self.gf('django.db.models.fields.IntegerField')()), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('foldit', ['Score']) + + # Adding model 'PuzzleComplete' + db.create_table('foldit_puzzlecomplete', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='foldit_puzzles_complete', to=orm['auth.User'])), + ('unique_user_id', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)), + ('puzzle_id', self.gf('django.db.models.fields.IntegerField')()), + ('puzzle_set', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + ('puzzle_subset', self.gf('django.db.models.fields.IntegerField')(db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('foldit', ['PuzzleComplete']) + + # Adding unique constraint on 'PuzzleComplete', fields ['user', 'puzzle_id', 'puzzle_set', 'puzzle_subset'] + db.create_unique('foldit_puzzlecomplete', ['user_id', 'puzzle_id', 'puzzle_set', 'puzzle_subset']) + + + def backwards(self, orm): + # Removing unique constraint on 'PuzzleComplete', fields ['user', 'puzzle_id', 'puzzle_set', 'puzzle_subset'] + db.delete_unique('foldit_puzzlecomplete', ['user_id', 'puzzle_id', 'puzzle_set', 'puzzle_subset']) + + # Deleting model 'Score' + db.delete_table('foldit_score') + + # Deleting model 'PuzzleComplete' + db.delete_table('foldit_puzzlecomplete') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'foldit.puzzlecomplete': { + 'Meta': {'ordering': "['puzzle_id']", 'unique_together': "(('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset'),)", 'object_name': 'PuzzleComplete'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'puzzle_id': ('django.db.models.fields.IntegerField', [], {}), + 'puzzle_set': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'puzzle_subset': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'unique_user_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'foldit_puzzles_complete'", 'to': "orm['auth.User']"}) + }, + 'foldit.score': { + 'Meta': {'object_name': 'Score'}, + 'best_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'current_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'puzzle_id': ('django.db.models.fields.IntegerField', [], {}), + 'score_version': ('django.db.models.fields.IntegerField', [], {}), + 'unique_user_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'foldit_scores'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['foldit'] \ No newline at end of file diff --git a/lms/djangoapps/foldit/migrations/__init__.py b/lms/djangoapps/foldit/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 414c62115ce3564028bda381e0d42c2f3f6f4889 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 25 Feb 2013 20:12:33 -0500 Subject: [PATCH 08/10] Fixed tests and list check --- lms/djangoapps/foldit/models.py | 2 ++ lms/djangoapps/foldit/tests.py | 39 +++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index df1be3e87c..703962f422 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -72,6 +72,8 @@ class Score(models.Model): [ {username: 'a_user', score: 12000} ...] """ + if not(type(puzzles) == list): + puzzles = [puzzles] scores = Score.objects \ .filter(puzzle_id__in=puzzles) \ .annotate(total_score=models.Sum('best_score')) \ diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 3a0fe54503..7127651601 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -45,6 +45,10 @@ class FolditTestCase(TestCase): Given lists of puzzle_ids and best_scores (must have same length), make a SetPlayerPuzzleScores request and return the response. """ + if not(type(best_scores) == list): + best_scores = [best_scores] + if not(type(puzzle_ids) == list): + puzzle_ids = [puzzle_ids] user = self.user if not user else user def score_dict(puzzle_id, best_score): @@ -62,7 +66,7 @@ class FolditTestCase(TestCase): data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), 'SetPlayerPuzzleScores': scores_str} - request = self.make_request(data) + request = self.make_request(data, user) response = foldit_ops(request) self.assertEqual(response.status_code, 200) @@ -70,9 +74,9 @@ class FolditTestCase(TestCase): def test_SetPlayerPuzzleScores(self): - puzzle_id = [994391] + puzzle_id = 994391 best_score = 0.078034 - response = self.make_puzzle_score_request([puzzle_id], [best_score]) + response = self.make_puzzle_score_request(puzzle_id, [best_score]) self.assertEqual(response.content, json.dumps( [{"OperationID": "SetPlayerPuzzleScores", @@ -81,7 +85,7 @@ class FolditTestCase(TestCase): "Status": "Success"}]}])) # There should now be a score in the db. - top_10 = Score.get_tops_n(puzzle_id, 10) + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(best_score)) @@ -105,11 +109,11 @@ class FolditTestCase(TestCase): (keep latest for each user, have multiple users work properly) """ orig_score = 0.07 - puzzle_id = ['1'] + puzzle_id = '1' response = self.make_puzzle_score_request([puzzle_id], [orig_score]) # There should now be a score in the db. - top_10 = Score.get_tops_n(puzzle_id, 10) + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(orig_score)) @@ -117,20 +121,26 @@ class FolditTestCase(TestCase): better_score = 0.06 response = self.make_puzzle_score_request([1], [better_score]) - top_10 = Score.get_tops_n(puzzle_id, 10) + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) - self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) + + # Floats always get in the way, so do almostequal + self.assertAlmostEqual(top_10[0]['score'], + Score.display_score(better_score), + delta=0.5) # reporting a worse score shouldn't worse_score = 0.065 response = self.make_puzzle_score_request([1], [worse_score]) - top_10 = Score.get_tops_n(puzzle_id, 10) + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) # should still be the better score - self.assertEqual(top_10[0]['score'], Score.display_score(better_score)) + self.assertAlmostEqual(top_10[0]['score'], + Score.display_score(better_score), + delta=0.5) - def test_SetPlayerPyzzleScores_manyplayers(self): + def test_SetPlayerPuzzleScores_manyplayers(self): """ Check that when we send scores from multiple users, the correct order of scores is displayed. @@ -138,18 +148,19 @@ class FolditTestCase(TestCase): puzzle_id = ['1'] player1_score = 0.07 player2_score = 0.08 - response1 = self.make_puzzle_score_request([puzzle_id], [player1_score], + response1 = self.make_puzzle_score_request(puzzle_id, player1_score, self.user) # There should now be a score in the db. - top_10 = Score.get_tops_n(puzzle_id, 10) + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(player1_score)) - response2 = self.make_puzzle_score_request([puzzle_id], [player2_score], + response2 = self.make_puzzle_score_request(puzzle_id, player2_score, self.user2) # There should now be two scores in the db + top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 2) # Top score should be player2_score. Second should be player1_score From adaa8463e3fa90e25f177f2f589170bfac02366e Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Tue, 26 Feb 2013 10:03:20 -0500 Subject: [PATCH 09/10] Removed commented-out fn --- lms/djangoapps/foldit/models.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index 703962f422..7041be1446 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -38,24 +38,6 @@ class Score(models.Model): """ return (-score) * 10 + 8000 * sum_of - # TODO: delete this, incorporate it in get_tops_n - #@staticmethod - #def get_top_n(puzzle_id, n): - #""" - #Arguments: - #puzzle_id (int): id of the puzzle for which to look - #n (int): number of top scores to return. - - #Returns: - #The top (lowest energy, highest display score) n scores for the puzzle. If - #there are fewer than n, returns all. Output is a list of dictionaries, sorted - #by display_score: - #[ {username: 'a_user', - #score: 8500}, ...] - #""" - #scores = Score.objects.filter(puzzle_id=puzzle_id).order_by('-best_score')[:n] - #return [{'username': s.user.username, 'score': Score.display_score(s.best_score)} - #for s in scores] @staticmethod def get_tops_n(n, puzzles=['994559']): From b6f3042c1d16f32e78279b0a576c13d6cde5248d Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Tue, 26 Feb 2013 12:38:09 -0500 Subject: [PATCH 10/10] Incorporate Victor's suggestions --- common/lib/xmodule/xmodule/foldit_module.py | 4 ++-- lms/djangoapps/foldit/views.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 7f46a34b0f..3990a61183 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -96,8 +96,8 @@ class FolditModule(XModule): self.required_level, self.required_sublevel) - showbasic = (self.metadata.get("show_basic_score") == "true") - showleader = (self.metadata.get("show_leaderboard") == "true") + showbasic = (self.metadata.get("show_basic_score").lower() == "true") + showleader = (self.metadata.get("show_leaderboard").lower() == "true") context = { 'due': self.due_str, 'success': self.is_complete(), diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index 9939d1aa63..988c113d23 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -132,7 +132,6 @@ def save_scores(user, puzzle_scores): score_version=score_version) obj.save() - # TODO: get info from db instead? score_responses.append({'PuzzleID': puzzle_id, 'Status': 'Success'})