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: