Merge pull request #1555 from MITx/feature/jkarni/foldit-api
Feature/jkarni/foldit api
This commit is contained in:
20
common/lib/xmodule/xmodule/css/foldit/leaderboard.scss
Normal file
20
common/lib/xmodule/xmodule/css/foldit/leaderboard.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,27 @@ from xmodule.xml_module import XmlDescriptor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FolditModule(XModule):
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.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:
|
||||
<foldit show_basic_score="true"
|
||||
required_level="4"
|
||||
required_sublevel="3"
|
||||
show_leaderboard="false"/>
|
||||
"""
|
||||
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():
|
||||
"""
|
||||
@@ -66,6 +79,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['score']) for e in Score.get_tops_n(10)]
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
@@ -75,15 +96,47 @@ class FolditModule(XModule):
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
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(),
|
||||
'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)
|
||||
|
||||
return self.system.render_template('foldit.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):
|
||||
"""
|
||||
@@ -97,9 +150,10 @@ class FolditModule(XModule):
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
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
|
||||
@@ -119,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}
|
||||
|
||||
111
lms/djangoapps/foldit/migrations/0001_initial.py
Normal file
111
lms/djangoapps/foldit/migrations/0001_initial.py
Normal file
@@ -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']
|
||||
0
lms/djangoapps/foldit/migrations/__init__.py
Normal file
0
lms/djangoapps/foldit/migrations/__init__.py
Normal file
@@ -25,6 +25,47 @@ class Score(models.Model):
|
||||
score_version = models.IntegerField()
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@staticmethod
|
||||
def display_score(score, sum_of=1):
|
||||
"""
|
||||
Argument:
|
||||
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
|
||||
"""
|
||||
return (-score) * 10 + 8000 * sum_of
|
||||
|
||||
|
||||
@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 <puzzles>. Output is a list
|
||||
of disctionaries, sorted by display_score:
|
||||
[ {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')) \
|
||||
.order_by('-total_score')[:n]
|
||||
num = len(puzzles)
|
||||
|
||||
return [{'username': s.user.username,
|
||||
'score': Score.display_score(s.total_score, num)}
|
||||
for s in scores]
|
||||
|
||||
|
||||
class PuzzleComplete(models.Model):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -25,92 +25,162 @@ 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=None):
|
||||
request = self.factory.post(self.url, post_data)
|
||||
request.user = self.user
|
||||
request.user = self.user if not user else user
|
||||
return request
|
||||
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
return {"PuzzleID": puzzle_id,
|
||||
"ScoreType": "score",
|
||||
"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(user.email, scores_str),
|
||||
"VerifyMethod": "FoldItVerify"}
|
||||
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
|
||||
'SetPlayerPuzzleScores': scores_str}
|
||||
|
||||
request = self.make_request(data, user)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def test_SetPlayerPuzzleScores(self):
|
||||
|
||||
scores = [ {"PuzzleID": 994391,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078034,
|
||||
"CurrentScore":0.080035,
|
||||
"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)
|
||||
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_tops_n(10, puzzle_id)
|
||||
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_tops_n(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 1)
|
||||
self.assertEqual(top_10[0]['score'], Score.display_score(orig_score))
|
||||
|
||||
# Reporting a better score should overwrite
|
||||
better_score = 0.06
|
||||
response = self.make_puzzle_score_request([1], [better_score])
|
||||
|
||||
top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 1)
|
||||
|
||||
# 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(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 1)
|
||||
# should still be the better score
|
||||
self.assertAlmostEqual(top_10[0]['score'],
|
||||
Score.display_score(better_score),
|
||||
delta=0.5)
|
||||
|
||||
def test_SetPlayerPuzzleScores_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(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,
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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,31 @@ def save_scores(user, puzzle_scores):
|
||||
# BestScore (energy), CurrentScore (Energy), ScoreVersion (int)
|
||||
|
||||
puzzle_id = score['PuzzleID']
|
||||
|
||||
# TODO: save the score
|
||||
best_score = score['BestScore']
|
||||
current_score = score['CurrentScore']
|
||||
score_version = score['ScoreVersion']
|
||||
|
||||
# SetPlayerPuzzleScoreResponse object
|
||||
# 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()
|
||||
|
||||
score_responses.append({'PuzzleID': puzzle_id,
|
||||
'Status': 'Success'})
|
||||
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
<section class="foldit">
|
||||
<p><strong>Due:</strong> ${due}
|
||||
|
||||
% if show_basic:
|
||||
${folditbasic}
|
||||
% endif
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
% if success:
|
||||
You have successfully gotten to level ${goal_level}.
|
||||
% else:
|
||||
You have not yet gotten to level ${goal_level}.
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<h3>Completed puzzles</h3>
|
||||
% if show_leader:
|
||||
${folditchallenge}
|
||||
% endif
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Submitted</th>
|
||||
</tr>
|
||||
% for puzzle in completed:
|
||||
<tr>
|
||||
<td>${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}</td>
|
||||
<td>${puzzle['created'].strftime('%Y-%m-%d %H:%M')}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
29
lms/templates/folditbasic.html
Normal file
29
lms/templates/folditbasic.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="folditbasic">
|
||||
<p><strong>Due:</strong> ${due}
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
% if success:
|
||||
You have successfully gotten to level ${goal_level}.
|
||||
% else:
|
||||
You have not yet gotten to level ${goal_level}.
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<h3>Completed puzzles</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Submitted</th>
|
||||
</tr>
|
||||
% for puzzle in completed:
|
||||
<tr>
|
||||
<td>${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}</td>
|
||||
<td>${puzzle['created'].strftime('%Y-%m-%d %H:%M')}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
|
||||
</br>
|
||||
</div>
|
||||
16
lms/templates/folditchallenge.html
Normal file
16
lms/templates/folditchallenge.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="folditchallenge">
|
||||
<h3>Puzzle Leaderboard</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
% for pair in top_scores:
|
||||
<tr>
|
||||
<td>${pair[0]}</td>
|
||||
<td>${pair[1]}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user