Remove 'Fold It' XModule
This commit is contained in:
@@ -36,7 +36,6 @@ XMODULES = [
|
||||
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
|
||||
"videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor",
|
||||
"imageannotation = xmodule.imageannotation_module:ImageAnnotationDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
|
||||
"hidden = xmodule.hidden_module:HiddenDescriptor",
|
||||
"raw = xmodule.raw_module:RawDescriptor",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
$leaderboard: #F4F4F4;
|
||||
|
||||
section.foldit {
|
||||
div.folditchallenge {
|
||||
table {
|
||||
border: 1px solid lighten($leaderboard, 10%);
|
||||
border-collapse: collapse;
|
||||
margin-top: $baseline;
|
||||
}
|
||||
th {
|
||||
background: $leaderboard;
|
||||
color: darken($leaderboard, 25%);
|
||||
}
|
||||
td {
|
||||
background: lighten($leaderboard, 3%);
|
||||
border-bottom: 1px solid $white;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xblock.fields import Scope, Integer, String
|
||||
from .fields import Date
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FolditFields(object):
|
||||
# default to what Spring_7012x uses
|
||||
required_level_half_credit = Integer(default=3, scope=Scope.settings)
|
||||
required_sublevel_half_credit = Integer(default=5, scope=Scope.settings)
|
||||
required_level = Integer(default=4, scope=Scope.settings)
|
||||
required_sublevel = Integer(default=5, scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
|
||||
show_basic_score = String(scope=Scope.settings, default='false')
|
||||
show_leaderboard = String(scope=Scope.settings, default='false')
|
||||
|
||||
|
||||
class FolditModule(FolditFields, XModule):
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Example:
|
||||
<foldit show_basic_score="true"
|
||||
required_level="4"
|
||||
required_sublevel="3"
|
||||
required_level_half_credit="2"
|
||||
required_sublevel_half_credit="3"
|
||||
show_leaderboard="false"/>
|
||||
"""
|
||||
super(FolditModule, self).__init__(*args, **kwargs)
|
||||
self.due_time = self.due
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
Did the user get to the required level before the due date?
|
||||
"""
|
||||
# We normally don't want django dependencies in xmodule. foldit is
|
||||
# special. Import this late to avoid errors with things not yet being
|
||||
# initialized.
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
complete = PuzzleComplete.is_level_complete(
|
||||
self.system.anonymous_student_id,
|
||||
self.required_level,
|
||||
self.required_sublevel,
|
||||
self.due_time)
|
||||
return complete
|
||||
|
||||
def is_half_complete(self):
|
||||
"""
|
||||
Did the user reach the required level for half credit?
|
||||
|
||||
Ideally this would be more flexible than just 0, 0.5, or 1 credit. On
|
||||
the other hand, the xml attributes for specifying more specific
|
||||
cut-offs and partial grades can get more confusing.
|
||||
"""
|
||||
from foldit.models import PuzzleComplete
|
||||
complete = PuzzleComplete.is_level_complete(
|
||||
self.system.anonymous_student_id,
|
||||
self.required_level_half_credit,
|
||||
self.required_sublevel_half_credit,
|
||||
self.due_time)
|
||||
return complete
|
||||
|
||||
def completed_puzzles(self):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
dicts:
|
||||
|
||||
[ {'set': int,
|
||||
'subset': int,
|
||||
'created': datetime} ]
|
||||
|
||||
The list is sorted by set, then subset
|
||||
"""
|
||||
from foldit.models import PuzzleComplete
|
||||
|
||||
return sorted(
|
||||
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
|
||||
key=lambda d: (d['set'], d['subset']))
|
||||
|
||||
def puzzle_leaders(self, n=10, courses=None):
|
||||
"""
|
||||
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
|
||||
|
||||
if courses is None:
|
||||
courses = [self.location.course_key]
|
||||
|
||||
leaders = [(leader['username'], leader['score']) for leader in Score.get_tops_n(10, course_list=courses)]
|
||||
leaders.sort(key=lambda x: -x[1])
|
||||
|
||||
return leaders
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Render the html for the module.
|
||||
"""
|
||||
goal_level = '{0}-{1}'.format(
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
showbasic = (self.show_basic_score.lower() == "true")
|
||||
showleader = (self.show_leaderboard.lower() == "true")
|
||||
|
||||
context = {
|
||||
'due': self.due,
|
||||
'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,
|
||||
'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):
|
||||
"""
|
||||
0 if required_level_half_credit - required_sublevel_half_credit not
|
||||
reached.
|
||||
0.5 if required_level_half_credit and required_sublevel_half_credit
|
||||
reached.
|
||||
1 if requred_level and required_sublevel reached.
|
||||
"""
|
||||
if self.is_complete():
|
||||
score = 1
|
||||
elif self.is_half_complete():
|
||||
score = 0.5
|
||||
else:
|
||||
score = 0
|
||||
return {'score': score,
|
||||
'total': self.max_score()}
|
||||
|
||||
def max_score(self):
|
||||
return 1
|
||||
|
||||
|
||||
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding Foldit problems to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = FolditModule
|
||||
filename_extension = "xml"
|
||||
|
||||
has_score = True
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
# The grade changes without any student interaction with the edx website,
|
||||
# so always need to actually check.
|
||||
always_recalculate_grades = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {}, []
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('foldit')
|
||||
return xml_object
|
||||
@@ -1810,45 +1810,6 @@ CREATE TABLE `external_auth_externalauthmap` (
|
||||
CONSTRAINT `external_auth_externala_user_id_644e7779f2d52b9a_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `foldit_puzzlecomplete`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `foldit_puzzlecomplete` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`unique_user_id` varchar(50) NOT NULL,
|
||||
`puzzle_id` int(11) NOT NULL,
|
||||
`puzzle_set` int(11) NOT NULL,
|
||||
`puzzle_subset` int(11) NOT NULL,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `foldit_puzzlecomplete_user_id_4c63656af6674331_uniq` (`user_id`,`puzzle_id`,`puzzle_set`,`puzzle_subset`),
|
||||
KEY `foldit_puzzlecomplete_ff2b2d15` (`unique_user_id`),
|
||||
KEY `foldit_puzzlecomplete_56c088b4` (`puzzle_set`),
|
||||
KEY `foldit_puzzlecomplete_2dc27ffb` (`puzzle_subset`),
|
||||
CONSTRAINT `foldit_puzzlecomplete_user_id_cd0294fb3a392_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `foldit_score`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `foldit_score` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`unique_user_id` varchar(50) NOT NULL,
|
||||
`puzzle_id` int(11) NOT NULL,
|
||||
`best_score` double NOT NULL,
|
||||
`current_score` double NOT NULL,
|
||||
`score_version` int(11) NOT NULL,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `foldit_score_user_id_6ac502fe1f6861b2_fk_auth_user_id` (`user_id`),
|
||||
KEY `foldit_score_ff2b2d15` (`unique_user_id`),
|
||||
KEY `foldit_score_44726e86` (`best_score`),
|
||||
KEY `foldit_score_32d6f808` (`current_score`),
|
||||
CONSTRAINT `foldit_score_user_id_6ac502fe1f6861b2_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `instructor_task_instructortask`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
@@ -378,8 +378,7 @@ def _grade(student, request, course, keep_raw_scores, field_data_cache, scores_c
|
||||
|
||||
with outer_atomic():
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.,
|
||||
# combinedopenended)
|
||||
# with the LMS, so they need to always be scored. (E.g. combinedopenended ORA1)
|
||||
# TODO This block is causing extra savepoints to be fired that are empty because no queries are executed
|
||||
# during the loop. When refactoring this code please keep this outer_atomic call in mind and ensure we
|
||||
# are not making unnecessary database queries.
|
||||
@@ -699,7 +698,7 @@ def get_score(user, problem_descriptor, module_creator, scores_client, submissio
|
||||
return submissions_scores_cache[location_url]
|
||||
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.)
|
||||
# with the LMS, so they need to always be scored. (E.g. combinedopenended ORA1.)
|
||||
if problem_descriptor.always_recalculate_grades:
|
||||
problem = module_creator(problem_descriptor)
|
||||
if problem is None:
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PuzzleComplete',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('unique_user_id', models.CharField(max_length=50, db_index=True)),
|
||||
('puzzle_id', models.IntegerField()),
|
||||
('puzzle_set', models.IntegerField(db_index=True)),
|
||||
('puzzle_subset', models.IntegerField(db_index=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(related_name='foldit_puzzles_complete', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['puzzle_id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Score',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('unique_user_id', models.CharField(max_length=50, db_index=True)),
|
||||
('puzzle_id', models.IntegerField()),
|
||||
('best_score', models.FloatField(db_index=True)),
|
||||
('current_score', models.FloatField(db_index=True)),
|
||||
('score_version', models.IntegerField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(related_name='foldit_scores', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='puzzlecomplete',
|
||||
unique_together=set([('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset')]),
|
||||
),
|
||||
]
|
||||
@@ -1,142 +0,0 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Score(models.Model):
|
||||
"""
|
||||
This model stores the scores of different users on FoldIt problems.
|
||||
"""
|
||||
user = models.ForeignKey(User, db_index=True,
|
||||
related_name='foldit_scores')
|
||||
|
||||
# The XModule that wants to access this doesn't have access to the real
|
||||
# userid. Save the anonymized version so we can look up by that.
|
||||
unique_user_id = models.CharField(max_length=50, db_index=True)
|
||||
puzzle_id = models.IntegerField()
|
||||
best_score = models.FloatField(db_index=True)
|
||||
current_score = models.FloatField(db_index=True)
|
||||
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'], course_list=None):
|
||||
"""
|
||||
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>,
|
||||
filtered by course. If no courses is specified we default
|
||||
the pool of students to all courses. Output is a list
|
||||
of dictionaries, sorted by display_score:
|
||||
[ {username: 'a_user',
|
||||
score: 12000} ...]
|
||||
"""
|
||||
|
||||
if not isinstance(puzzles, list):
|
||||
puzzles = [puzzles]
|
||||
if course_list is None:
|
||||
scores = Score.objects \
|
||||
.filter(puzzle_id__in=puzzles) \
|
||||
.annotate(total_score=models.Sum('best_score')) \
|
||||
.order_by('total_score')[:n]
|
||||
else:
|
||||
scores = Score.objects \
|
||||
.filter(puzzle_id__in=puzzles) \
|
||||
.filter(user__courseenrollment__course_id__in=course_list) \
|
||||
.annotate(total_score=models.Sum('best_score')) \
|
||||
.order_by('total_score')[:n]
|
||||
num = len(puzzles)
|
||||
|
||||
return [
|
||||
{'username': score.user.username,
|
||||
'score': Score.display_score(score.total_score, num)}
|
||||
for score in scores
|
||||
]
|
||||
|
||||
|
||||
class PuzzleComplete(models.Model):
|
||||
"""
|
||||
This keeps track of the sets of puzzles completed by each user.
|
||||
|
||||
e.g. PuzzleID 1234, set 1, subset 3. (Sets and subsets correspond to levels
|
||||
in the intro puzzles)
|
||||
"""
|
||||
class Meta(object):
|
||||
# there should only be one puzzle complete entry for any particular
|
||||
# puzzle for any user
|
||||
unique_together = ('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset')
|
||||
ordering = ['puzzle_id']
|
||||
|
||||
user = models.ForeignKey(User, db_index=True,
|
||||
related_name='foldit_puzzles_complete')
|
||||
|
||||
# The XModule that wants to access this doesn't have access to the real
|
||||
# userid. Save the anonymized version so we can look up by that.
|
||||
unique_user_id = models.CharField(max_length=50, db_index=True)
|
||||
puzzle_id = models.IntegerField()
|
||||
puzzle_set = models.IntegerField(db_index=True)
|
||||
puzzle_subset = models.IntegerField(db_index=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return "PuzzleComplete({0}, id={1}, set={2}, subset={3}, created={4})".format(
|
||||
self.user.username, self.puzzle_id,
|
||||
self.puzzle_set, self.puzzle_subset,
|
||||
self.created)
|
||||
|
||||
@staticmethod
|
||||
def completed_puzzles(anonymous_user_id):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
dicts:
|
||||
|
||||
[ {'set': int,
|
||||
'subset': int,
|
||||
'created': datetime} ]
|
||||
"""
|
||||
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id)
|
||||
return [{'set': c.puzzle_set,
|
||||
'subset': c.puzzle_subset,
|
||||
'created': c.created} for c in complete]
|
||||
|
||||
@staticmethod
|
||||
def is_level_complete(anonymous_user_id, level, sub_level, due=None):
|
||||
"""
|
||||
Return True if this user completed level--sub_level by due.
|
||||
|
||||
Users see levels as e.g. 4-5.
|
||||
|
||||
Args:
|
||||
level: int
|
||||
sub_level: int
|
||||
due (optional): If specified, a datetime. Ignored if None.
|
||||
"""
|
||||
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id,
|
||||
puzzle_set=level,
|
||||
puzzle_subset=sub_level)
|
||||
if due is not None:
|
||||
complete = complete.filter(created__lte=due)
|
||||
|
||||
return complete.exists()
|
||||
@@ -1,403 +0,0 @@
|
||||
"""Tests for the FoldIt module"""
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from foldit.views import foldit_ops, verify_code
|
||||
from foldit.models import PuzzleComplete, Score
|
||||
from student.models import unique_id_for_user, CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import UTC
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FolditTestCase(TestCase):
|
||||
"""Tests for various responses of the FoldIt module"""
|
||||
def setUp(self):
|
||||
super(FolditTestCase, self).setUp()
|
||||
|
||||
self.factory = RequestFactory()
|
||||
self.url = reverse('foldit_ops')
|
||||
|
||||
self.course_id = SlashSeparatedCourseKey('course', 'id', '1')
|
||||
self.course_id2 = SlashSeparatedCourseKey('course', 'id', '2')
|
||||
|
||||
self.user = UserFactory.create()
|
||||
self.user2 = UserFactory.create()
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course_id)
|
||||
CourseEnrollment.enroll(self.user2, self.course_id2)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
self.tomorrow = now + timedelta(days=1)
|
||||
self.yesterday = now - timedelta(days=1)
|
||||
|
||||
def make_request(self, post_data, user=None):
|
||||
"""Makes a request to foldit_ops with the given post data and user (if specified)"""
|
||||
request = self.factory.post(self.url, post_data)
|
||||
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 isinstance(best_scores, list):
|
||||
best_scores = [best_scores]
|
||||
if not isinstance(puzzle_ids, list):
|
||||
puzzle_ids = [puzzle_ids]
|
||||
user = self.user if not user else user
|
||||
|
||||
def score_dict(puzzle_id, best_score):
|
||||
"""Returns a valid json-parsable score dict"""
|
||||
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): # pylint: disable=invalid-name
|
||||
|
||||
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": 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): # pylint: disable=invalid-name
|
||||
|
||||
response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000])
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{
|
||||
"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [
|
||||
{
|
||||
"PuzzleID": 1,
|
||||
"Status": "Success"
|
||||
}, {
|
||||
"PuzzleID": 2,
|
||||
"Status": "Success"
|
||||
}
|
||||
]
|
||||
}]
|
||||
))
|
||||
|
||||
def test_SetPlayerPuzzleScores_multiple(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
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'
|
||||
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
|
||||
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
|
||||
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_multiple_courses(self): # pylint: disable=invalid-name
|
||||
puzzle_id = "1"
|
||||
|
||||
player1_score = 0.05
|
||||
player2_score = 0.06
|
||||
|
||||
course_list_1 = [self.course_id]
|
||||
course_list_2 = [self.course_id2]
|
||||
|
||||
self.make_puzzle_score_request(puzzle_id, player1_score, self.user)
|
||||
|
||||
course_1_top_10 = Score.get_tops_n(10, puzzle_id, course_list_1)
|
||||
course_2_top_10 = Score.get_tops_n(10, puzzle_id, course_list_2)
|
||||
total_top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
|
||||
# player1 should now be in the top 10 of course 1 and not in course 2
|
||||
self.assertEqual(len(course_1_top_10), 1)
|
||||
self.assertEqual(len(course_2_top_10), 0)
|
||||
self.assertEqual(len(total_top_10), 1)
|
||||
|
||||
self.make_puzzle_score_request(puzzle_id, player2_score, self.user2)
|
||||
|
||||
course_2_top_10 = Score.get_tops_n(10, puzzle_id, course_list_2)
|
||||
total_top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
|
||||
# player2 should now be in the top 10 of course 2 and not in course 1
|
||||
self.assertEqual(len(course_1_top_10), 1)
|
||||
self.assertEqual(len(course_2_top_10), 1)
|
||||
self.assertEqual(len(total_top_10), 2)
|
||||
|
||||
def test_SetPlayerPuzzleScores_many_players(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Check that when we send scores from multiple users, the correct order
|
||||
of scores is displayed. Note that, before being processed by
|
||||
display_score, lower scores are better.
|
||||
"""
|
||||
puzzle_id = ['1']
|
||||
player1_score = 0.08
|
||||
player2_score = 0.02
|
||||
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))
|
||||
|
||||
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.assertAlmostEqual(
|
||||
top_10[0]['score'],
|
||||
Score.display_score(player2_score),
|
||||
delta=0.5
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
top_10[1]['score'],
|
||||
Score.display_score(player1_score),
|
||||
delta=0.5
|
||||
)
|
||||
|
||||
# Top score user should be self.user2.username
|
||||
self.assertEqual(top_10[0]['username'], self.user2.username)
|
||||
|
||||
def test_SetPlayerPuzzleScores_error(self): # pylint: disable=invalid-name
|
||||
|
||||
scores = [{
|
||||
"PuzzleID": 994391,
|
||||
"ScoreType": "score",
|
||||
"BestScore": 0.078034,
|
||||
"CurrentScore": 0.080035,
|
||||
"ScoreVersion": 23
|
||||
}]
|
||||
validation_str = json.dumps(scores)
|
||||
|
||||
verify = {
|
||||
"Verify": verify_code(self.user.email, validation_str),
|
||||
"VerifyMethod": "FoldItVerify"
|
||||
}
|
||||
|
||||
# change the real string -- should get an error
|
||||
scores[0]['ScoreVersion'] = 22
|
||||
scores_str = json.dumps(scores)
|
||||
|
||||
data = {
|
||||
'SetPlayerPuzzleScoresVerify': json.dumps(verify),
|
||||
'SetPlayerPuzzleScores': scores_str
|
||||
}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
json.dumps([{
|
||||
"OperationID": "SetPlayerPuzzleScores",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"}]))
|
||||
|
||||
def make_puzzles_complete_request(self, puzzles):
|
||||
"""
|
||||
Make a puzzles complete request, given an array of
|
||||
puzzles. E.g.
|
||||
|
||||
[ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
|
||||
"""
|
||||
puzzles_str = json.dumps(puzzles)
|
||||
|
||||
verify = {
|
||||
"Verify": verify_code(self.user.email, puzzles_str),
|
||||
"VerifyMethod": "FoldItVerify"
|
||||
}
|
||||
|
||||
data = {
|
||||
'SetPuzzlesCompleteVerify': json.dumps(verify),
|
||||
'SetPuzzlesComplete': puzzles_str
|
||||
}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def set_puzzle_complete_response(values):
|
||||
"""Returns a json response of a Puzzle Complete message"""
|
||||
return json.dumps([{"OperationID": "SetPuzzlesComplete",
|
||||
"Value": values}])
|
||||
|
||||
def test_SetPlayerPuzzlesComplete(self): # pylint: disable=invalid-name
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_multiple(self): # pylint: disable=invalid-name
|
||||
"""Check that state is stored properly"""
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 14, "Set": 1, "SubSet": 3},
|
||||
{"PuzzleID": 15, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(
|
||||
response.content,
|
||||
self.set_puzzle_complete_response([13, 14, 15, 53524])
|
||||
)
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_level_complete(self): # pylint: disable=invalid-name
|
||||
"""Check that the level complete function works"""
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 53524]))
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 14, "Set": 1, "SubSet": 3},
|
||||
{"PuzzleID": 15, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
self.set_puzzle_complete_response([13, 14, 15, 53524]))
|
||||
|
||||
is_complete = partial(
|
||||
PuzzleComplete.is_level_complete, unique_id_for_user(self.user))
|
||||
|
||||
self.assertTrue(is_complete(1, 1))
|
||||
self.assertTrue(is_complete(1, 3))
|
||||
self.assertTrue(is_complete(1, 2))
|
||||
self.assertFalse(is_complete(4, 5))
|
||||
|
||||
puzzles = [{"PuzzleID": 74, "Set": 4, "SubSet": 5}]
|
||||
|
||||
response = self.make_puzzles_complete_request(puzzles)
|
||||
|
||||
self.assertTrue(is_complete(4, 5))
|
||||
|
||||
# Now check due dates
|
||||
|
||||
self.assertTrue(is_complete(1, 1, due=self.tomorrow))
|
||||
self.assertFalse(is_complete(1, 1, due=self.yesterday))
|
||||
|
||||
def test_SetPlayerPuzzlesComplete_error(self): # pylint: disable=invalid-name
|
||||
|
||||
puzzles = [
|
||||
{"PuzzleID": 13, "Set": 1, "SubSet": 2},
|
||||
{"PuzzleID": 53524, "Set": 1, "SubSet": 1}
|
||||
]
|
||||
|
||||
puzzles_str = json.dumps(puzzles)
|
||||
|
||||
verify = {
|
||||
"Verify": verify_code(self.user.email, puzzles_str + "x"),
|
||||
"VerifyMethod": "FoldItVerify"
|
||||
}
|
||||
|
||||
data = {
|
||||
'SetPuzzlesCompleteVerify': json.dumps(verify),
|
||||
'SetPuzzlesComplete': puzzles_str
|
||||
}
|
||||
|
||||
request = self.make_request(data)
|
||||
|
||||
response = foldit_ops(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
json.dumps([{
|
||||
"OperationID": "SetPuzzlesComplete",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"}]))
|
||||
@@ -1,175 +0,0 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
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__)
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def foldit_ops(request):
|
||||
"""
|
||||
Endpoint view for foldit operations.
|
||||
"""
|
||||
responses = []
|
||||
if "SetPlayerPuzzleScores" in request.POST:
|
||||
puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores")
|
||||
pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify")
|
||||
log.debug("SetPlayerPuzzleScores message: puzzle scores: %r",
|
||||
puzzle_scores_json)
|
||||
|
||||
puzzle_score_verify = json.loads(pz_verify_json)
|
||||
if not verifies_ok(request.user.email,
|
||||
puzzle_scores_json, puzzle_score_verify):
|
||||
responses.append({"OperationID": "SetPlayerPuzzleScores",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning(
|
||||
"Verification of SetPlayerPuzzleScores failed:"
|
||||
"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, r':"\g<1>",', puzzle_scores_json)
|
||||
puzzle_scores = json.loads(puzzle_scores_json)
|
||||
responses.append(save_scores(request.user, puzzle_scores))
|
||||
|
||||
if "SetPuzzlesComplete" in request.POST:
|
||||
puzzles_complete_json = request.POST.get("SetPuzzlesComplete")
|
||||
pc_verify_json = request.POST.get("SetPuzzlesCompleteVerify")
|
||||
|
||||
log.debug("SetPuzzlesComplete message: %r",
|
||||
puzzles_complete_json)
|
||||
|
||||
puzzles_complete_verify = json.loads(pc_verify_json)
|
||||
|
||||
if not verifies_ok(request.user.email,
|
||||
puzzles_complete_json, puzzles_complete_verify):
|
||||
responses.append({"OperationID": "SetPuzzlesComplete",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning(
|
||||
"Verification of SetPuzzlesComplete failed:"
|
||||
" user %s, puzzles json %r, verify %r",
|
||||
request.user,
|
||||
puzzles_complete_json,
|
||||
pc_verify_json
|
||||
)
|
||||
else:
|
||||
puzzles_complete = json.loads(puzzles_complete_json)
|
||||
responses.append(save_complete(request.user, puzzles_complete))
|
||||
|
||||
return HttpResponse(json.dumps(responses))
|
||||
|
||||
|
||||
def verify_code(email, val):
|
||||
"""
|
||||
Given the email and passed in value (str), return the expected
|
||||
verification code.
|
||||
"""
|
||||
# TODO: is this the right string?
|
||||
verification_string = email.lower() + '|' + val
|
||||
return hashlib.md5(verification_string).hexdigest()
|
||||
|
||||
|
||||
def verifies_ok(email, val, verification):
|
||||
"""
|
||||
Check that the hash_str matches the expected hash of val.
|
||||
|
||||
Returns True if verification ok, False otherwise
|
||||
"""
|
||||
if verification.get("VerifyMethod") != "FoldItVerify":
|
||||
log.debug("VerificationMethod in %r isn't FoldItVerify", verification)
|
||||
return False
|
||||
hash_str = verification.get("Verify")
|
||||
|
||||
return verify_code(email, val) == hash_str
|
||||
|
||||
|
||||
def save_scores(user, puzzle_scores):
|
||||
score_responses = []
|
||||
for score in puzzle_scores:
|
||||
log.debug("score: %s", score)
|
||||
# expected keys ScoreType, PuzzleID (int),
|
||||
# BestScore (energy), CurrentScore (Energy), ScoreVersion (int)
|
||||
|
||||
puzzle_id = score['PuzzleID']
|
||||
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'})
|
||||
|
||||
return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses}
|
||||
|
||||
|
||||
def save_complete(user, puzzles_complete):
|
||||
"""
|
||||
Returned list of PuzzleIDs should be in sorted order (I don't think client
|
||||
cares, but tests do)
|
||||
"""
|
||||
for complete in puzzles_complete:
|
||||
log.debug("Puzzle complete: %s", complete)
|
||||
puzzle_id = complete['PuzzleID']
|
||||
puzzle_set = complete['Set']
|
||||
puzzle_subset = complete['SubSet']
|
||||
|
||||
# create if not there
|
||||
PuzzleComplete.objects.get_or_create(
|
||||
user=user,
|
||||
unique_user_id=unique_id_for_user(user),
|
||||
puzzle_id=puzzle_id,
|
||||
puzzle_set=puzzle_set,
|
||||
puzzle_subset=puzzle_subset)
|
||||
|
||||
# List of all puzzle ids of intro-level puzzles completed ever, including on this
|
||||
# request
|
||||
# TODO: this is just in this request...
|
||||
|
||||
complete_responses = list(pc.puzzle_id
|
||||
for pc in PuzzleComplete.objects.filter(user=user))
|
||||
|
||||
return {"OperationID": "SetPuzzlesComplete", "Value": complete_responses}
|
||||
@@ -1872,9 +1872,6 @@ INSTALLED_APPS = (
|
||||
#'wiki.plugins.notifications',
|
||||
'course_wiki.plugins.markdownedx',
|
||||
|
||||
# Foldit integration
|
||||
'foldit',
|
||||
|
||||
# For testing
|
||||
'django.contrib.admin', # only used in DEBUG mode
|
||||
'django_nose',
|
||||
|
||||
@@ -647,17 +647,3 @@ section.self-assessment {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
section.foldit {
|
||||
table {
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
th {
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
padding-left: ($baseline/4);
|
||||
padding-right: ($baseline/4);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<section class="foldit">
|
||||
|
||||
% if show_basic:
|
||||
${folditbasic}
|
||||
% endif
|
||||
|
||||
|
||||
% if show_leader:
|
||||
${folditchallenge}
|
||||
% endif
|
||||
|
||||
</section>
|
||||
@@ -1,33 +0,0 @@
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from util.date_utils import get_default_time_display
|
||||
%>
|
||||
<div class="folditbasic">
|
||||
<p><strong>${_("Due:")}</strong> ${get_default_time_display(due)}
|
||||
|
||||
<p>
|
||||
<strong>${_("Status:")}</strong>
|
||||
% if success:
|
||||
${_('You have successfully gotten to level {goal_level}.').format(goal_level=goal_level)}'
|
||||
% else:
|
||||
${_('You have not yet gotten to level {goal_level}.').format(goal_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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<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>
|
||||
@@ -651,12 +651,6 @@ if settings.FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'):
|
||||
url(r'^edinsights_service/', include('edinsights.core.urls')),
|
||||
)
|
||||
|
||||
# FoldIt views
|
||||
urlpatterns += (
|
||||
# The path is hardcoded into their app...
|
||||
url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"),
|
||||
)
|
||||
|
||||
if settings.FEATURES.get('ENABLE_DEBUG_RUN_PYTHON'):
|
||||
urlpatterns += (
|
||||
url(r'^debug/run_python$', 'debug.views.run_python'),
|
||||
|
||||
Reference in New Issue
Block a user