Commit 84a96e40 by Sarina Canelake

Remove 'Fold It' XModule

parent 2cfeb34f
......@@ -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",
......
$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;
}
}
}
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:
......
# -*- 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')]),
),
]
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()
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);
}
}
<section class="foldit">
% if show_basic:
${folditbasic}
% endif
% if show_leader:
${folditchallenge}
% endif
</section>
<%!
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>
<%! 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'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment