Foldit integration.
- ops view for the desktop app to talk to
- xmodule that talks to the foldit model and displays the student's state
- grading tweak to make grade updates from an external service work:
- Add an always_recalculate_grades property to XModuleDescriptor.
This commit is contained in:
committed by
Victor Shnayder
parent
fffbb55944
commit
5e44846596
0
common/djangoapps/foldit/__init__.py
Normal file
0
common/djangoapps/foldit/__init__.py
Normal file
95
common/djangoapps/foldit/models.py
Normal file
95
common/djangoapps/foldit/models.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
# 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()
|
||||
|
||||
263
common/djangoapps/foldit/tests.py
Normal file
263
common/djangoapps/foldit/tests.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
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 student.models import UserProfile, unique_id_for_user
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FolditTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.url = reverse('foldit_ops')
|
||||
|
||||
pwd = 'abc'
|
||||
self.user = User.objects.create_user('testuser', 'test@test.com', pwd)
|
||||
self.unique_user_id = unique_id_for_user(self.user)
|
||||
now = datetime.now()
|
||||
self.tomorrow = now + timedelta(days=1)
|
||||
self.yesterday = now - timedelta(days=1)
|
||||
|
||||
UserProfile.objects.create(user=self.user)
|
||||
|
||||
def make_request(self, post_data):
|
||||
request = self.factory.post(self.url, post_data)
|
||||
request.user = self.user
|
||||
return request
|
||||
|
||||
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)
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [{
|
||||
"PuzzleID": 994391,
|
||||
"Status": "Success"}]}]))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [{
|
||||
"PuzzleID": 994391,
|
||||
"Status": "Success"},
|
||||
|
||||
{"PuzzleID": 994392,
|
||||
"Status": "Success"}]}]))
|
||||
|
||||
|
||||
|
||||
def test_SetPlayerPuzzleScores_error(self):
|
||||
|
||||
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)
|
||||
|
||||
response_data = json.loads(response.content)
|
||||
|
||||
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):
|
||||
return json.dumps([{"OperationID":"SetPuzzlesComplete",
|
||||
"Value": values}])
|
||||
|
||||
|
||||
def test_SetPlayerPuzzlesComplete(self):
|
||||
|
||||
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):
|
||||
"""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):
|
||||
"""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, self.unique_user_id)
|
||||
|
||||
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):
|
||||
|
||||
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)
|
||||
|
||||
response_data = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.content,
|
||||
json.dumps([{
|
||||
"OperationID": "SetPuzzlesComplete",
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"}]))
|
||||
129
common/djangoapps/foldit/views.py
Normal file
129
common/djangoapps/foldit/views.py
Normal file
@@ -0,0 +1,129 @@
|
||||
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 foldit.models import Score, PuzzleComplete
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def foldit_ops(request):
|
||||
log.debug(request.POST)
|
||||
|
||||
responses = []
|
||||
if "SetPlayerPuzzleScores" in request.POST:
|
||||
puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores")
|
||||
pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify")
|
||||
|
||||
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.info("Verification of SetPlayerPuzzleScores failed:" +
|
||||
"user %s, scores json %r, verify %r",
|
||||
request.user, puzzle_scores_json, pz_verify_json)
|
||||
else:
|
||||
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")
|
||||
|
||||
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.info("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']
|
||||
|
||||
# TODO: save the score
|
||||
|
||||
# SetPlayerPuzzleScoreResponse object
|
||||
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}
|
||||
@@ -41,7 +41,8 @@ setup(
|
||||
"static_tab = xmodule.html_module:StaticTabDescriptor",
|
||||
"custom_tag_template = xmodule.raw_module:RawDescriptor",
|
||||
"about = xmodule.html_module:AboutDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor"
|
||||
]
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
129
common/lib/xmodule/xmodule/foldit_module.py
Normal file
129
common/lib/xmodule/xmodule/foldit_module.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
from dateutil import parser
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FolditModule(XModule):
|
||||
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
|
||||
|
||||
def parse_due_date():
|
||||
"""
|
||||
Pull out the date, or None
|
||||
"""
|
||||
s = self.metadata.get("due")
|
||||
if s:
|
||||
return parser.parse(s)
|
||||
else:
|
||||
return None
|
||||
|
||||
self.due_str = self.metadata.get("due", "None")
|
||||
self.due = parse_due_date()
|
||||
|
||||
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)
|
||||
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 get_html(self):
|
||||
"""
|
||||
Render the html for the module.
|
||||
"""
|
||||
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('foldit.html', context)
|
||||
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
0 / 1 based on whether student has gotten far enough.
|
||||
"""
|
||||
score = 1 if self.is_complete() else 0
|
||||
return {'score': score,
|
||||
'total': self.max_score()}
|
||||
|
||||
def max_score(self):
|
||||
return 1
|
||||
|
||||
|
||||
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding open ended response questions to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = FolditModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "foldit"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@property
|
||||
def always_recalculate_grades(self):
|
||||
"""
|
||||
The grade changes without any student interaction with the edx website,
|
||||
so always need to actually check.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
For now, don't need anything from the xml
|
||||
"""
|
||||
return {}
|
||||
@@ -645,6 +645,17 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def always_recalculate_grades(self):
|
||||
"""
|
||||
Return whether this descriptor always requires recalculation of grades,
|
||||
for example if the score can change via an extrnal service, not just
|
||||
when the student interacts with the module on the page. A specific
|
||||
example is FoldIt, which posts grade-changing updates through a separate
|
||||
API.
|
||||
"""
|
||||
return False
|
||||
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
|
||||
@@ -339,6 +339,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
|
||||
Can return None if user doesn't have access, or if something else went wrong.
|
||||
cache: A StudentModuleCache
|
||||
"""
|
||||
if problem_descriptor.always_recalculate_grades:
|
||||
problem = module_creator(problem_descriptor)
|
||||
d = problem.get_score()
|
||||
if d is not None:
|
||||
return (d['score'], d['total'])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
|
||||
# These are not problems, and do not have a score
|
||||
return (None, None)
|
||||
|
||||
@@ -590,6 +590,9 @@ INSTALLED_APPS = (
|
||||
'wiki.plugins.notifications',
|
||||
'course_wiki.plugins.markdownedx',
|
||||
|
||||
# foldit integration
|
||||
'foldit',
|
||||
|
||||
# For testing
|
||||
'django.contrib.admin', # only used in DEBUG mode
|
||||
|
||||
|
||||
@@ -248,3 +248,17 @@ section.self-assessment {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
section.foldit {
|
||||
table {
|
||||
margin-top: 10px;
|
||||
}
|
||||
th {
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
}
|
||||
}
|
||||
28
lms/templates/foldit.html
Normal file
28
lms/templates/foldit.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<section class="foldit">
|
||||
<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>
|
||||
|
||||
</section>
|
||||
@@ -288,7 +288,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
# Open Ended problem list
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
|
||||
'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
|
||||
|
||||
|
||||
# Cohorts management
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts$',
|
||||
'course_groups.views.list_cohorts', name="cohorts"),
|
||||
@@ -369,6 +369,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
|
||||
)
|
||||
|
||||
# FoldIt views
|
||||
urlpatterns += (
|
||||
# The path is hardcoded into their app...
|
||||
url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user