diff --git a/common/djangoapps/foldit/__init__.py b/common/djangoapps/foldit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/djangoapps/foldit/models.py b/common/djangoapps/foldit/models.py new file mode 100644 index 0000000000000000000000000000000000000000..ea4f099216c497627c2c02a6259d74520de06233 --- /dev/null +++ b/common/djangoapps/foldit/models.py @@ -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() + diff --git a/common/djangoapps/foldit/tests.py b/common/djangoapps/foldit/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..4d387b44e85b9f027f0fedae6715b2c43e68014d --- /dev/null +++ b/common/djangoapps/foldit/tests.py @@ -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"}])) diff --git a/common/djangoapps/foldit/views.py b/common/djangoapps/foldit/views.py new file mode 100644 index 0000000000000000000000000000000000000000..62e60ee0de1eb832065f50cf0eb6f5e2da6c0d21 --- /dev/null +++ b/common/djangoapps/foldit/views.py @@ -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} diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index a1b059b8891078fd3e56c429b0d61531e21a4c28..061e93fd21e867b38d9208d8c834d8cb4707f652 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -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", + ] } ) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py new file mode 100644 index 0000000000000000000000000000000000000000..2be8fd599011239ff7e6a26f8ecb48eb89bc320f --- /dev/null +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -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 {} diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 0e4e8e0f008dc87cb328da0f02e9f19576c76fbe..874a603c66f661478c32833f11039a32b349cf3c 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -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): diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index f532e6c530bbe5c0d83730060ea16e872fa95fa4..72876ff23d41a3db1e3c135e336b20ed33e5aa90 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -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) diff --git a/lms/envs/common.py b/lms/envs/common.py index 16472795e05bb976653e1639acd6ff1112114124..a22c426314a6d8456f5fcc3faec2b5e71772888d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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 diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 038903b756dc57c78f1278823e46783159d48710..ea987d8b2fdbbc7f5af937aa8ae03d908cf4362f 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -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; + + } +} \ No newline at end of file diff --git a/lms/templates/foldit.html b/lms/templates/foldit.html new file mode 100644 index 0000000000000000000000000000000000000000..2c16ebbfebdfc9de758859bcb9b2a87f6a36ed49 --- /dev/null +++ b/lms/templates/foldit.html @@ -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> \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 2d4267ec71d5d09522a7f703c0a167e16305cd05..f4e098c9f6f9674e97131ef4dc78a81434a267d0 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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: