From aa0e49e94ba19166e2dcf23f81af59b7558d70d7 Mon Sep 17 00:00:00 2001
From: Andreas Kloeckner <inform@tiker.net>
Date: Sun, 12 Oct 2014 01:04:20 -0500
Subject: [PATCH] Allow using data files in Python code questions

---
 TODO                       |  2 --
 cfrunpy/cfrunpy            |  4 ++--
 cfrunpy/cfrunpy_backend.py | 22 ++++++++++++----------
 course/page.py             | 32 +++++++++++++++++++++++++++++---
 4 files changed, 43 insertions(+), 17 deletions(-)

diff --git a/TODO b/TODO
index b04cdc43..4054973c 100644
--- a/TODO
+++ b/TODO
@@ -2,8 +2,6 @@
 
 - Fix docker reliability
 
-- Supply data to cfrunpy
-
 - Unanswered questions for unanswered statistics q?
 
 - flow overview
diff --git a/cfrunpy/cfrunpy b/cfrunpy/cfrunpy
index cf8f0051..c9529236 100755
--- a/cfrunpy/cfrunpy
+++ b/cfrunpy/cfrunpy
@@ -29,7 +29,7 @@ import socketserver
 import json
 import sys
 import io
-from cfrunpy_backend import dict_to_struct, run_code, package_exception
+from cfrunpy_backend import Struct, run_code, package_exception
 from http.server import BaseHTTPRequestHandler
 
 PORT = 9941
@@ -76,7 +76,7 @@ class RunRequestHandler(BaseHTTPRequestHandler):
 
             print("CFRUNPY RECEIVED %d bytes" % len(recv_data),
                     file=prev_stderr)
-            run_req = dict_to_struct(json.loads(recv_data.decode("utf-8")))
+            run_req = Struct(json.loads(recv_data.decode("utf-8")))
             print("REQUEST: %r" % run_req, file=prev_stderr)
 
             stdout = io.StringIO()
diff --git a/cfrunpy/cfrunpy_backend.py b/cfrunpy/cfrunpy_backend.py
index cfa27180..1e5c94c5 100644
--- a/cfrunpy/cfrunpy_backend.py
+++ b/cfrunpy/cfrunpy_backend.py
@@ -42,6 +42,10 @@ PROTOCOL
 
     .. attribute:: test_code
 
+    .. attribute:: data_files
+
+        a dictionary from data file names to their contents
+
     .. attribute:: compile_only
 
         :class:`bool`
@@ -110,20 +114,11 @@ PROTOCOL
 class Struct(object):
     def __init__(self, entries):
         for name, val in entries.items():
-            self.__dict__[name] = dict_to_struct(val)
+            self.__dict__[name] = val
 
     def __repr__(self):
         return repr(self.__dict__)
 
-
-def dict_to_struct(data):
-    if isinstance(data, list):
-        return [dict_to_struct(d) for d in data]
-    elif isinstance(data, dict):
-        return Struct(data)
-    else:
-        return data
-
 # }}}
 
 
@@ -194,10 +189,17 @@ def run_code(result, run_req):
 
     # {{{ run code
 
+    data_files = {}
+    if hasattr(run_req, "data_files"):
+        from base64 import b64decode
+        for name, contents in run_req.data_files.items():
+            data_files[name] = b64decode(contents.encode())
+
     feedback = Feedback()
     maint_ctx = {
             "feedback": feedback,
             "user_code": user_code,
+            "data_files": data_files,
             "GradingComplete": GradingComplete,
             }
 
diff --git a/course/page.py b/course/page.py
index 8995549c..ff2005d4 100644
--- a/course/page.py
+++ b/course/page.py
@@ -28,7 +28,7 @@ from course.validation import validate_struct, ValidationError, validate_markup
 from course.content import remove_prefix
 from django.utils.safestring import mark_safe
 import django.forms as forms
-from django.contrib import messages
+from django.core.exceptions import ObjectDoesNotExist
 
 from courseflow.utils import StyledForm, Struct
 
@@ -1346,6 +1346,18 @@ def request_python_run(run_req, run_timeout):
 
 
 class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
+    def __init__(self, vctx, location, page_desc):
+        super(PythonCodeQuestion, self).__init__(vctx, location, page_desc)
+
+        if vctx is not None and hasattr(page_desc, "data_files"):
+            for data_file in page_desc.data_files:
+                try:
+                    from course.content import get_repo_blob
+                    get_repo_blob(vctx.repo, data_file, vctx.commit_sha)
+                except ObjectDoesNotExist:
+                    raise ValidationError("%s: data file '%s' not found"
+                            % (location, data_file))
+
     def required_attrs(self):
         return super(PythonCodeQuestion, self).required_attrs() + (
                 ("prompt", "markup"),
@@ -1360,6 +1372,7 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
                 ("test_code", str),
                 ("correct_code", str),
                 ("initial_code", str),
+                ("data_files", list),
                 )
 
     def _initial_code(self):
@@ -1431,6 +1444,19 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
         transfer_attr("names_from_user")
         transfer_attr("test_code")
 
+        if hasattr(self.page_desc, "data_files"):
+            run_req["data_files"] = {}
+
+            from course.content import get_repo_blob
+
+            for data_file in self.page_desc.data_files:
+                from base64 import b64encode
+                run_req["data_files"][data_file] = \
+                        b64encode(
+                                get_repo_blob(
+                                    page_context.repo, data_file,
+                                    page_context.commit_sha).data)
+
         try:
             response_dict = request_python_run(run_req,
                     run_timeout=self.page_desc.timeout)
@@ -1597,8 +1623,8 @@ class PythonCodeQuestionWithHumanTextFeedback(
         if (vctx is not None
                 and self.page_desc.human_feedback_value > self.page_desc.value):
             raise ValidationError(
-                    "human_feedback_value greater than overall "
-                    "value of question")
+                    "%s: human_feedback_value greater than overall "
+                    "value of question" % location)
 
     def required_attrs(self):
         return super(
-- 
GitLab