From 87be6aecdab851f62e2b015c5afa8c516bfd38e7 Mon Sep 17 00:00:00 2001 From: ericsonga Date: Sat, 1 Jul 2017 07:25:17 +0200 Subject: [PATCH 1/4] Removed line that wasn't needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the disabling of the help button. It should have been inside a conditional and only executed if the problem was adaptive. But, now that I have made the help button always active it isn’t needed. --- runestone/parsons/js/parsons.js | 1 - 1 file changed, 1 deletion(-) diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index 75a2fc213..3fb6da08d 100755 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -126,7 +126,6 @@ LineBasedGrader.prototype.grade = function() { feedbackArea.html("Perfect! It took you only one try to solve this. Great job!"); } correct = true; - problem.helpButton.disabled = false; // bje } else { // Incorrect Indention state = "incorrectIndent"; From 60577c9dc1a4e5e86556dce53d57f5fd2fcd5722 Mon Sep 17 00:00:00 2001 From: ericsonga Date: Sat, 1 Jul 2017 14:55:16 +0200 Subject: [PATCH 2/4] Removed the runestone class div which was messing up the width of the Parsons problems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I found the place where the div class=runestone was added and removed it. This was limiting the Parsons problems to a width of 500 px and we need them to be as large as the activecode. Otherwise you can’t have very long code in your Parsons problems. --- runestone/parsons/parsons.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py index 3ee1f0ccc..63d308811 100755 --- a/runestone/parsons/parsons.py +++ b/runestone/parsons/parsons.py @@ -109,11 +109,9 @@ def findmax(alist): addQuestionToDB(self) TEMPLATE = ''' -
         %(qnumber)s: %(instructions)s%(code)s
         
-
''' self.options['divid'] = self.arguments[0] self.options['qnumber'] = self.getNumber() From 47bc90d0e9703ba7e704af4461751f0c9f0f7b7d Mon Sep 17 00:00:00 2001 From: ericsonga Date: Mon, 3 Jul 2017 09:04:51 +0200 Subject: [PATCH 3/4] Merge remote-tracking branch 'RunestoneInteractive/master' # Conflicts: # runestone/parsons/parsons.py --- runestone/activecode/js/activecode.js | 2 +- runestone/assess/js/mchoice.js | 2 +- runestone/assess/js/timed.js | 5 +++++ runestone/clickableArea/js/clickable.js | 2 +- runestone/dragndrop/js/dragndrop.js | 2 +- runestone/fitb/js/fitb.js | 2 +- runestone/parsons/js/parsons.js | 2 +- runestone/parsons/parsons.py | 4 ++++ runestone/shortanswer/js/shortanswer.js | 2 +- 9 files changed, 16 insertions(+), 7 deletions(-) diff --git a/runestone/activecode/js/activecode.js b/runestone/activecode/js/activecode.js index 30efff919..280a115f2 100755 --- a/runestone/activecode/js/activecode.js +++ b/runestone/activecode/js/activecode.js @@ -1824,7 +1824,7 @@ ACFactory.toggleScratchActivecode = function () { $(document).ready(function() { ACFactory.createScratchActivecode(); $('[data-component=activecode]').each( function(index ) { - if ($(this.parentNode).data("component") !== "timedAssessment" && $(this.parentNode.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0 ) { // If this element exists within a timed component, don't render it here edList[this.id] = ACFactory.createActiveCode(this, $(this).data('lang')); } }); diff --git a/runestone/assess/js/mchoice.js b/runestone/assess/js/mchoice.js index dd76607e5..2c7c7f252 100644 --- a/runestone/assess/js/mchoice.js +++ b/runestone/assess/js/mchoice.js @@ -553,7 +553,7 @@ MultipleChoice.prototype.compareAnswers = function () { $(document).bind("runestone:login-complete", function () { $("[data-component=multiplechoice]").each(function (index) { // MC var opts = {"orig": this, 'useRunestoneServices':eBookConfig.useRunestoneServices}; - if ($(this.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0) { // If this element exists within a timed component, don't render it here mcList[this.id] = new MultipleChoice(opts); } }); diff --git a/runestone/assess/js/timed.js b/runestone/assess/js/timed.js index 5ba16da4e..8ca2b0f4f 100644 --- a/runestone/assess/js/timed.js +++ b/runestone/assess/js/timed.js @@ -318,9 +318,14 @@ Timed.prototype.renderFeedbackContainer = function () { Timed.prototype.createRenderedQuestionArray = function () { // this finds all the assess questions in this timed assessment and calls their constructor method // Also adds them to this.renderedQuestionArray + // todo: This needs to be updated to account for the runestone div wrapper. for (var i = 0; i < this.newChildren.length; i++) { var tmpChild = this.newChildren[i]; opts = {'orig':tmpChild, 'useRunestoneServices':eBookConfig.useRunestoneServices} + if ($(tmpChild).children("[data-component]")) { + tmpChild = $(tmpChild).children("[data-component]")[0]; + opts.orig = tmpChild; + } if ($(tmpChild).is("[data-component=multiplechoice]")) { this.renderedQuestionArray.push({"question": new TimedMC(opts)}); } else if ($(tmpChild).is("[data-component=fillintheblank]")) { diff --git a/runestone/clickableArea/js/clickable.js b/runestone/clickableArea/js/clickable.js index 2871a643f..d4fa90bf5 100644 --- a/runestone/clickableArea/js/clickable.js +++ b/runestone/clickableArea/js/clickable.js @@ -378,7 +378,7 @@ ClickableArea.prototype.renderFeedback = function () { =================================*/ $(document).bind("runestone:login-complete", function () { $("[data-component=clickablearea]").each(function (index) { - if ($(this.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0) { // If this element exists within a timed component, don't render it here CAList[this.id] = new ClickableArea({"orig": this, "useRunestoneServices":eBookConfig.useRunestoneServices}); } }); diff --git a/runestone/dragndrop/js/dragndrop.js b/runestone/dragndrop/js/dragndrop.js index 9db105f1b..905d7ba12 100644 --- a/runestone/dragndrop/js/dragndrop.js +++ b/runestone/dragndrop/js/dragndrop.js @@ -425,7 +425,7 @@ DragNDrop.prototype.setLocalStorage = function (data) { $(document).bind("runestone:login-complete", function () { $("[data-component=dragndrop]").each(function (index) { var opts = {"orig": this, 'useRunestoneServices':eBookConfig.useRunestoneServices}; - if ($(this.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0) { // If this element exists within a timed component, don't render it here ddList[this.id] = new DragNDrop(opts); } }); diff --git a/runestone/fitb/js/fitb.js b/runestone/fitb/js/fitb.js index a33b5643e..b5f786362 100644 --- a/runestone/fitb/js/fitb.js +++ b/runestone/fitb/js/fitb.js @@ -400,7 +400,7 @@ FITB.prototype.compareFITB = function (data, status, whatever) { // Creates a $(document).bind("runestone:login-complete", function () { $("[data-component=fillintheblank]").each(function (index) { var opts = {"orig" : this, "useRunestoneServices": eBookConfig.useRunestoneServices}; - if ($(this.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0) { // If this element exists within a timed component, don't render it here FITBList[this.id] = new FITB(opts); } }); diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index 3fb6da08d..ba6f9436a 100755 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -3002,7 +3002,7 @@ Parsons.prototype.resetView = function() { $(document).bind("runestone:login-complete", function () { $("[data-component=parsons]").each(function (index) { - if ($(this.parentNode).data("component") !== "timedAssessment") { + if ($(this).closest('[data-component=timedAssessment]').length == 0) { prsList[this.id] = new Parsons({"orig": this, "useRunestoneServices": eBookConfig.useRunestoneServices}); } }); diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py index 63d308811..72f6426ad 100755 --- a/runestone/parsons/parsons.py +++ b/runestone/parsons/parsons.py @@ -109,6 +109,10 @@ def findmax(alist): addQuestionToDB(self) TEMPLATE = ''' +<<<<<<< HEAD +======= +
+>>>>>>> RunestoneInteractive/master
         %(qnumber)s: %(instructions)s%(code)s
         
diff --git a/runestone/shortanswer/js/shortanswer.js b/runestone/shortanswer/js/shortanswer.js index 0d947a543..feb2577f6 100644 --- a/runestone/shortanswer/js/shortanswer.js +++ b/runestone/shortanswer/js/shortanswer.js @@ -200,7 +200,7 @@ ShortAnswer.prototype.restoreAnswers = function (data) { =================================*/ $(document).ready(function () { $("[data-component=shortanswer]").each(function (index) { - if ($(this.parentNode).data("component") !== "timedAssessment") { // If this element exists within a timed component, don't render it here + if ($(this).closest('[data-component=timedAssessment]').length == 0) { // If this element exists within a timed component, don't render it here saList[this.id] = new ShortAnswer({"orig": this, 'useRunestoneServices': eBookConfig.useRunestoneServices}); } }); From a86fbb7445a36ad1ef77f697100341eec9241ead Mon Sep 17 00:00:00 2001 From: ericsonga Date: Mon, 17 Jul 2017 12:41:45 +0200 Subject: [PATCH 4/4] Merge remote-tracking branch 'RunestoneInteractive/master' # Conflicts: # runestone/parsons/parsons.py --- .gitignore | 1 + README.rst | 8 +- requirements.txt | 9 +- runTests.sh | 27 +- runestone/__init__.py | 3 +- runestone/__main__.py | 1 + runestone/activecode/activecode.py | 23 +- runestone/activecode/js/activecode.js | 73 +--- runestone/activecode/test/__init__.py | 0 runestone/activecode/test/build_info | 1 - runestone/activecode/test/conf.py | 10 + runestone/activecode/test/pavement.py | 2 +- runestone/activecode/test/test_activecode.py | 24 +- runestone/assess/js/mchoice.js | 3 +- runestone/assess/multiplechoice.py | 1 - runestone/assess/test/__init__.py | 0 runestone/assess/test/build_info | 1 - runestone/assess/test/conf.py | 11 + runestone/assess/test/pavement.py | 2 +- runestone/assess/test/test_assess.py | 27 +- runestone/clickableArea/clickable.py | 11 +- runestone/clickableArea/js/clickable.js | 1 + runestone/clickableArea/test/__init__.py | 0 runestone/clickableArea/test/build_info | 1 - runestone/clickableArea/test/conf.py | 11 + runestone/clickableArea/test/pavement.py | 2 +- .../clickableArea/test/test_clickableArea.py | 48 +-- runestone/codelens/js/pytutor.js | 4 + runestone/codelens/visualizer.py | 7 +- runestone/common/js/runestonebase.js | 47 ++- runestone/common/project_template/conf.tmpl | 12 +- .../common/project_template/pavement.tmpl | 2 +- runestone/datafile/__init__.py | 9 +- runestone/dragndrop/dragndrop.py | 14 +- runestone/dragndrop/test/__init__.py | 0 runestone/dragndrop/test/build_info | 1 - runestone/dragndrop/test/conf.py | 11 + runestone/dragndrop/test/pavement.py | 2 +- runestone/dragndrop/test/test_dragndrop.py | 26 +- runestone/external/external.py | 3 +- runestone/fitb/fitb.py | 368 ++++++++++-------- runestone/fitb/js/fitb.js | 186 +++------ runestone/fitb/test/__init__.py | 0 runestone/fitb/test/_sources/index.rst | 15 +- runestone/fitb/test/build_info | 1 - runestone/fitb/test/conf.py | 11 + runestone/fitb/test/pavement.py | 2 +- runestone/fitb/test/test_fitb.py | 112 +++--- runestone/parsons/js/parsons.js | 1 + runestone/parsons/parsons.py | 44 ++- runestone/poll/js/poll.js | 1 + runestone/poll/test/__init__.py | 0 runestone/poll/test/build_info | 1 - runestone/poll/test/conf.py | 11 + runestone/poll/test/pavement.py | 2 +- runestone/poll/test/test_poll.py | 25 +- runestone/question/test/build_info | 1 - runestone/question/test/conf.py | 11 + runestone/question/test/pavement.py | 2 +- runestone/question/test/test_question.py | 27 +- runestone/reveal/test/__init__.py | 0 runestone/reveal/test/build_info | 1 - runestone/reveal/test/conf.py | 11 + runestone/reveal/test/pavement.py | 2 +- runestone/reveal/test/test_reveal.py | 27 +- runestone/server/__init__.py | 5 +- runestone/server/chapternames.py | 19 +- runestone/server/componentdb.py | 26 +- runestone/shortanswer/js/shortanswer.js | 3 +- runestone/shortanswer/test/__init__.py | 0 runestone/shortanswer/test/build_info | 1 - runestone/shortanswer/test/conf.py | 11 + runestone/shortanswer/test/pavement.py | 2 +- .../shortanswer/test/test_shortanswer.py | 28 +- runestone/tabbedStuff/test/__init__.py | 0 runestone/tabbedStuff/test/build_info | 1 - runestone/tabbedStuff/test/conf.py | 11 + runestone/tabbedStuff/test/pavement.py | 2 +- .../tabbedStuff/test/test_tabbedStuff.py | 35 +- runestone/unittest_base.py | 63 +++ runestone/usageAssignment/__init__.py | 13 +- runestone/video/video.py | 9 +- setup.py | 4 +- 83 files changed, 745 insertions(+), 789 deletions(-) create mode 100644 runestone/activecode/test/__init__.py delete mode 100644 runestone/activecode/test/build_info create mode 100644 runestone/assess/test/__init__.py delete mode 100644 runestone/assess/test/build_info create mode 100644 runestone/clickableArea/test/__init__.py delete mode 100644 runestone/clickableArea/test/build_info create mode 100644 runestone/dragndrop/test/__init__.py delete mode 100644 runestone/dragndrop/test/build_info create mode 100644 runestone/fitb/test/__init__.py delete mode 100644 runestone/fitb/test/build_info create mode 100644 runestone/poll/test/__init__.py delete mode 100644 runestone/poll/test/build_info delete mode 100644 runestone/question/test/build_info create mode 100644 runestone/reveal/test/__init__.py delete mode 100644 runestone/reveal/test/build_info create mode 100644 runestone/shortanswer/test/__init__.py delete mode 100644 runestone/shortanswer/test/build_info create mode 100644 runestone/tabbedStuff/test/__init__.py delete mode 100644 runestone/tabbedStuff/test/build_info create mode 100644 runestone/unittest_base.py diff --git a/.gitignore b/.gitignore index a487ff4b2..cc45ae8a2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ nosetests.xml .idea bower_componenets/ .venv +*~ runestone/build_info diff --git a/README.rst b/README.rst index 1648c89db..d1a51e4b9 100644 --- a/README.rst +++ b/README.rst @@ -115,10 +115,14 @@ Our goal is to have unit tests which rely on Selenium (a library that helps simu **In order to get started with writing a test/writing additional tests, you will need the following:** * ``pip install selenium`` in the virtualenv you're using for Runestone Components development +* ``pip install pyvirtualdisplay`` -* Download `PhantomJS `_., which is a driver that helps you simulate the browser. You can download it `here `_. -* You'll also need to have done the above installation. +* Download the latest `ChromeDriver `_., which is a driver that simulates Google Chrome. + +* On linux you will need to install Xvfb ``apt-get install xvfb`` + +* You'll also need to have done the above installation. * You should be using virtual environment, you'll need a clone of the RunestoneComponents repository, and you'll need to have done ``pip install -e`` from the top level of the RunestoneComponents directory. diff --git a/requirements.txt b/requirements.txt index 88e6b45cd..91ee2f756 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ -click==6.6 -Paver==1.2.4 -six==1.10.0 -Sphinx<1.6 +click +Paver>=1.2.4 +six +Sphinx>=1.6.3 sphinxcontrib-paverutils>=1.13 cogapp>=2.5 SQLAlchemy>=1.0.13 selenium>=2.53.6 +pyvirtualdisplay diff --git a/runTests.sh b/runTests.sh index 19124189a..323ab1a30 100755 --- a/runTests.sh +++ b/runTests.sh @@ -1,27 +1,2 @@ #!/usr/bin/env bash - -set -e -testhome=`pwd` -port=8081 -for t in 'activecode' 'assess' 'clickableArea' 'dragndrop' 'fitb' 'poll' 'question' 'reveal' 'shortanswer' 'tabbedStuff'; do - cd runestone/$t/test - runestone build --all - runestone serve --port $port & - SERVE_PID=$! - echo "Running test_${t}.py" $port - set -x - python "test_${t}.py" - if [ $? -ne 0 ]; then - echo "Test failed" - pgrep -lf '.*runestone serve.*' | awk '{ print $1 }' | xargs kill - exit 1 - else - echo "killing server" $SERVE_PID - kill $SERVE_PID - fi - set -e - cd $testhome - #port=$((port+1)) -done - -exit 0 +python -m unittest discover diff --git a/runestone/__init__.py b/runestone/__init__.py index dbaa59fda..66fc61aff 100644 --- a/runestone/__init__.py +++ b/runestone/__init__.py @@ -27,7 +27,7 @@ def runestone_static_dirs(): module_static_js = ['%s/js' % os.path.join(basedir,x) for x in module_paths if os.path.exists('%s/js' % os.path.join(basedir,x))] module_static_css = ['%s/css' % os.path.join(basedir,x) for x in module_paths if os.path.exists('%s/css' % os.path.join(basedir,x))] module_static_image = ['%s/images' % os.path.join(basedir,x) for x in module_paths if os.path.exists('%s/images' % os.path.join(basedir,x))] - module_static_bootstrap = ['%s/bootstrap' % os.path.join(basedir,x) for x in module_paths if os.path.exists('%s/bootstrap' % os.path.join(basedir,x))] + module_static_bootstrap = ['%s/bootstrap' % os.path.join(basedir,x) for x in module_paths if os.path.exists('%s/bootstrap' % os.path.join(basedir,x))] return module_static_js + module_static_css + module_static_image + module_static_bootstrap @@ -102,7 +102,6 @@ def build(options): cmap = {'activecode': ActiveCode, 'mchoice': MChoice, 'fillintheblank': FillInTheBlank, - 'blank': Blank, 'timed': TimedDirective, 'qnum': QuestionNumber, 'codelens': Codelens, diff --git a/runestone/__main__.py b/runestone/__main__.py index f21b94ba7..18f9a9556 100644 --- a/runestone/__main__.py +++ b/runestone/__main__.py @@ -120,6 +120,7 @@ def serve(port,listen): httpd.allow_reuse_address = True httpd.server_bind() httpd.server_activate() + sys.stderr = open('runestone.log','a') httpd.serve_forever() @cli.command() diff --git a/runestone/activecode/activecode.py b/runestone/activecode/activecode.py index 47914f07f..99f4fb8a5 100644 --- a/runestone/activecode/activecode.py +++ b/runestone/activecode/activecode.py @@ -23,7 +23,7 @@ from .textfield import * from sqlalchemy import create_engine, Table, MetaData, select, delete from runestone.server import get_dburl -from runestone.server.componentdb import addQuestionToDB, addHTMLToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB, engine, meta from runestone.common.runestonedirective import RunestoneDirective try: @@ -122,17 +122,6 @@ def process_activcode_nodes(app, env, docname): def purge_activecodes(app, env, docname): pass -database_connection = True -try: - engine = create_engine(get_dburl(locals())) - meta = MetaData() - Source_code = Table('source_code', meta, autoload=True, autoload_with=engine) - Div = Table('div_ids', meta, autoload=True, autoload_with=engine) -except: - print("Cannot connect") - database_connection = False - - class ActiveCode(RunestoneDirective): """ .. activecode:: uniqueid @@ -324,7 +313,8 @@ def run(self): course_name = env.config.html_context['course_id'] divid = self.options['divid'] - try: + if engine: + Source_code = Table('source_code', meta, autoload=True, autoload_with=engine) engine.execute(Source_code.delete().where(Source_code.c.acid == divid).where(Source_code.c.course_id == course_name)) engine.execute(Source_code.insert().values( acid = divid, @@ -339,6 +329,7 @@ def run(self): except: ch, sub_ch = (env.docname, 'null subchapter') + Div = Table('div_ids', meta, autoload=True, autoload_with=engine) engine.execute(Div.delete()\ .where(Div.c.course_name == course_name)\ .where(Div.c.chapter == ch)\ @@ -353,11 +344,7 @@ def run(self): )) - except Exception as e: - import traceback - print("The exception is ", e) - traceback.print_exc() - print(env.config.html_context['course_id']) + else: print("Unable to save to source_code table in activecode.py. Possible problems:") print(" 1. dburl or course_id are not set in conf.py for your book") print(" 2. unable to connect to the database using dburl") diff --git a/runestone/activecode/js/activecode.js b/runestone/activecode/js/activecode.js index 280a115f2..dc9bd7167 100755 --- a/runestone/activecode/js/activecode.js +++ b/runestone/activecode/js/activecode.js @@ -19,6 +19,7 @@ function ActiveCode(opts) { ActiveCode.prototype.init = function(opts) { RunestoneBase.apply( this, arguments ); // call parent constructor + RunestoneBase.prototype.init.apply(this, arguments); var suffStart; var orig = opts.orig; this.useRunestoneServices = opts.useRunestoneServices; @@ -31,8 +32,6 @@ ActiveCode.prototype.init = function(opts) { this.timelimit = $(orig).data('timelimit'); this.includes = $(orig).data('include'); this.hidecode = $(orig).data('hidecode'); - this.sid = opts.sid; - this.graderactive = opts.graderactive; this.runButton = null; this.saveButton = null; this.loadButton = null; @@ -289,7 +288,23 @@ ActiveCode.prototype.addHistoryScrubber = function (pos_last) { $(scrubber).on("slidechange",this.slideit.bind(this)); scrubberDiv.appendChild(scrubber); - if (pos_last) { + // If there is a deadline set then position the scrubber at the last submission + // prior to the deadline + if (this.deadline) { + let i = 0; + let done = false; + while (i < this.history.length && ! done) { + if ((new Date(this.timestamps[i])) > this.deadline) { + done = true; + } else { + i += 1 + } + } + i = i - 1; + scrubber.value = Math.max(i,0); + this.editor.setValue(this.history[scrubber.value]); + } + else if (pos_last) { scrubber.value = this.history.length-1; this.editor.setValue(this.history[scrubber.value]); } else { @@ -373,59 +388,7 @@ ActiveCode.prototype.addCaption = function() { this.outerDiv.parentNode.insertBefore(capDiv, this.outerDiv.nextSibling); }; -ActiveCode.prototype.saveEditor = function () { - var res; - var saveSuccess = function(data, status, whatever) { - if (data.redirect) { - alert("Did not save! It appears you are not logged in properly") - } else if (data == "") { - alert("Error: Program not saved"); - } - else { - var acid = eval(data)[0]; - if (acid.indexOf("ERROR:") == 0) { - alert(acid); - } else { - // use a tooltip to provide some success feedback - var save_btn = $(this.saveButton); - save_btn.attr('title', 'Saved your code.'); - var opts = { - 'trigger': 'manual', - 'placement': 'bottom', - 'delay': { show: 100, hide: 500} - }; - save_btn.tooltip(opts); - save_btn.tooltip('show'); - setTimeout(function () { - save_btn.tooltip('destroy') - }, 4000); - - $('#' + acid + ' .CodeMirror').css('border-top', '2px solid #aaa'); - $('#' + acid + ' .CodeMirror').css('border-bottom', '2px solid #aaa'); - } - } - }.bind(this); - - var data = {acid: this.divid, code: this.editor.getValue()}; - data.lang = this.language; - if (data.code.match(/^\s+$/)) { - res = confirm("You are about to save an empty program, this will overwrite a previously saved program. Continue?"); - if (! res) { - return; - } - } - $(document).ajaxError(function (e, jqhxr, settings, exception) { - //alert("Request Failed for" + settings.url) - console.log("Request Failed for" + settings.url); - }); - jQuery.post(eBookConfig.ajaxURL + 'saveprog', data, saveSuccess); - if (this.editor.acEditEvent) { - this.logBookEvent({'event': 'activecode', 'act': 'edit', 'div_id': this.divid}); // Log the run event - this.editor.acEditEvent = false; - } - this.logBookEvent({'event': 'activecode', 'act': 'save', 'div_id': this.divid}); // Log the run event -}; ActiveCode.prototype.loadEditor = function () { var loadEditor = (function (data, status, whatever) { diff --git a/runestone/activecode/test/__init__.py b/runestone/activecode/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/activecode/test/build_info b/runestone/activecode/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/activecode/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/activecode/test/conf.py b/runestone/activecode/test/conf.py index 143a13814..360962857 100644 --- a/runestone/activecode/test/conf.py +++ b/runestone/activecode/test/conf.py @@ -90,6 +90,16 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/activecode/test/pavement.py b/runestone/activecode/test/pavement.py index e54382b54..c02600d68 100644 --- a/runestone/activecode/test/pavement.py +++ b/runestone/activecode/test/pavement.py @@ -36,7 +36,7 @@ ) ) -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/activecode/test/test_activecode.py b/runestone/activecode/test/test_activecode.py index 46eca55dd..9efa88707 100644 --- a/runestone/activecode/test/test_activecode.py +++ b/runestone/activecode/test/test_activecode.py @@ -1,17 +1,9 @@ -from selenium import webdriver +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys -PORT = '8081' - -class ActiveCodeTests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT +setUpModule, tearDownModule = module_fixture_maker(__file__) +class ActiveCodeTests(RunestoneTestCase): def test_hello(self): ''' 1. Get the outer div id of the activecode component @@ -58,13 +50,3 @@ def test_history(self): rb.click() output = t1.find_element_by_class_name("ac_output") self.assertEqual(output.text.strip(), "Hello World") - - def tearDown(self): - self.driver.quit() - - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main() \ No newline at end of file diff --git a/runestone/assess/js/mchoice.js b/runestone/assess/js/mchoice.js index 2c7c7f252..d3ed6f598 100644 --- a/runestone/assess/js/mchoice.js +++ b/runestone/assess/js/mchoice.js @@ -28,13 +28,12 @@ MultipleChoice.prototype = new RunestoneBase(); MultipleChoice.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; // entire
    element this.origElem = orig; this.useRunestoneServices = opts.useRunestoneServices; this.multipleanswers = false; this.divid = orig.id; - this.sid = opts.sid; - this.graderactive = opts.graderactive; if ($(this.origElem).data("multipleanswers") === true) { this.multipleanswers = true; diff --git a/runestone/assess/multiplechoice.py b/runestone/assess/multiplechoice.py index 662715327..8c5ddfb19 100644 --- a/runestone/assess/multiplechoice.py +++ b/runestone/assess/multiplechoice.py @@ -206,7 +206,6 @@ def run(self): # .. code-block:: # :number-lines: # - # # mcNode = MChoiceNode() # Item 1 of problem text # ... diff --git a/runestone/assess/test/__init__.py b/runestone/assess/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/assess/test/build_info b/runestone/assess/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/assess/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/assess/test/conf.py b/runestone/assess/test/conf.py index 51418f10b..e0906278c 100644 --- a/runestone/assess/test/conf.py +++ b/runestone/assess/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/assess/test/pavement.py b/runestone/assess/test/pavement.py index 8641de746..dc12cbb0a 100644 --- a/runestone/assess/test/pavement.py +++ b/runestone/assess/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/assess/test/test_assess.py b/runestone/assess/test/test_assess.py index 4091679f6..d22d4de31 100644 --- a/runestone/assess/test/test_assess.py +++ b/runestone/assess/test/test_assess.py @@ -4,20 +4,11 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' - -class MultipleChoiceQuestion_Tests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT +setUpModule, tearDownModule = module_fixture_maker(__file__) +class MultipleChoiceQuestion_Tests(RunestoneTestCase): def test_ma1(self): '''Multiple Answer: Nothing selected, Check button clicked''' self.driver.get(self.host + "/index.html") @@ -130,7 +121,7 @@ def test_mc3(self): '''Multiple Choice: Incorrect answer selected''' self.driver.get(self.host + "/index.html") t1 = self.driver.find_element_by_id("question2") - + t1.find_element_by_id("question2_opt_1").click() btn_check = t1.find_element_by_tag_name('button') @@ -161,13 +152,3 @@ def test_mc4(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/clickableArea/clickable.py b/runestone/clickableArea/clickable.py index 85f1618c3..1defcbe1f 100644 --- a/runestone/clickableArea/clickable.py +++ b/runestone/clickableArea/clickable.py @@ -19,7 +19,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive -from runestone.server.componentdb import addQuestionToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB from runestone.common.runestonedirective import RunestoneDirective def setup(app): @@ -55,6 +55,9 @@ def __init__(self,content): def visit_ca_node(self,node): res = TEMPLATE + node.delimiter = "_start__{}_".format(node.ca_options['divid']) + self.body.append(node.delimiter) + if "feedback" in node.ca_options: node.ca_options["feedback"] = "" + node.ca_options["feedback"] + "" else: @@ -76,6 +79,12 @@ def depart_ca_node(self,node): res = TEMPLATE_END % node.ca_options self.body.append(res) + addHTMLToDB(node.ca_options['divid'], + node.ca_options['basecourse'], + "".join(self.body[self.body.index(node.delimiter) + 1:])) + + self.body.remove(node.delimiter) + class ClickableArea(RunestoneDirective): """ diff --git a/runestone/clickableArea/js/clickable.js b/runestone/clickableArea/js/clickable.js index d4fa90bf5..2f269ecef 100644 --- a/runestone/clickableArea/js/clickable.js +++ b/runestone/clickableArea/js/clickable.js @@ -25,6 +25,7 @@ ClickableArea.prototype = new RunestoneBase(); ClickableArea.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; // entire
    element that will be replaced by new HTML this.origElem = orig; this.divid = orig.id; diff --git a/runestone/clickableArea/test/__init__.py b/runestone/clickableArea/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/clickableArea/test/build_info b/runestone/clickableArea/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/clickableArea/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/clickableArea/test/conf.py b/runestone/clickableArea/test/conf.py index 767bb064f..b6c5deba4 100644 --- a/runestone/clickableArea/test/conf.py +++ b/runestone/clickableArea/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/clickableArea/test/pavement.py b/runestone/clickableArea/test/pavement.py index 35ff83333..b9e84d82e 100644 --- a/runestone/clickableArea/test/pavement.py +++ b/runestone/clickableArea/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/clickableArea/test/test_clickableArea.py b/runestone/clickableArea/test/test_clickableArea.py index 35011f888..b46dd5964 100644 --- a/runestone/clickableArea/test/test_clickableArea.py +++ b/runestone/clickableArea/test/test_clickableArea.py @@ -4,22 +4,12 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' +setUpModule, tearDownModule = module_fixture_maker(__file__) ANSWERS = ["Red Orange Yellow", "Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet"] -class ClickableAreaQuestion_Tests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT - - +class ClickableAreaQuestion_Tests(RunestoneTestCase): def test_ca1(self): '''Text/Code: Nothing selected''' self.driver.get(self.host + "/index.html") @@ -33,7 +23,7 @@ def test_ca1(self): cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - + def test_ca2(self): '''Text/Code: Correct answer(s) selected''' self.driver.get(self.host + "/index.html") @@ -56,8 +46,8 @@ def test_ca2(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-success", cnamestr) - - + + def test_ca3(self): '''Text/Code: Incorrect answer selected''' self.driver.get(self.host + "/index.html") @@ -81,7 +71,7 @@ def test_ca3(self): cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - + def test_ca4(self): '''Text/Code: All options clicked one by one''' self.driver.get(self.host + "/index.html") @@ -106,7 +96,7 @@ def test_ca4(self): cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - + def test_ca5(self): '''Text/Code: Correct answer selected and unselected''' self.driver.get(self.host + "/index.html") @@ -122,7 +112,7 @@ def test_ca5(self): if target.text in ANSWERS: cnamestr = target.get_attribute("class") self.assertNotIn("clickable-clicked", cnamestr) - + def test_ca6(self): '''Table: Nothing selected''' @@ -136,8 +126,8 @@ def test_ca6(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - - + + def test_ca7(self): '''Table: Correct answer(s) selected''' self.driver.get(self.host + "/index.html") @@ -160,7 +150,7 @@ def test_ca7(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-success", cnamestr) - + def test_ca8(self): '''Table: Incorrect answer selected''' @@ -184,7 +174,7 @@ def test_ca8(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - + def test_ca9(self): '''Table: All options clicked one by one''' @@ -209,7 +199,7 @@ def test_ca9(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-danger", cnamestr) - + def test_ca10(self): '''Table: Correct answer selected and unselected''' @@ -226,13 +216,3 @@ def test_ca10(self): if target.text in ANSWERS: cnamestr = target.get_attribute("class") self.assertNotIn("clickable-clicked", cnamestr) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/codelens/js/pytutor.js b/runestone/codelens/js/pytutor.js index 4ae5187a2..03455f858 100644 --- a/runestone/codelens/js/pytutor.js +++ b/runestone/codelens/js/pytutor.js @@ -4125,15 +4125,19 @@ function traceQCheckMe(inputId, divId, answer) { var ans = $('#'+inputId).val() var attrs = answer.split(".") var correctAns = curEntry; + var rb = new RunestoneBase() for (j in attrs) { correctAns = correctAns[attrs[j]] } + feedbackElement = $("#" + divId + "_feedbacktext") if (ans.length > 0 && ans == correctAns) { feedbackElement.html('Correct') } else { feedbackElement.html(vis.curTrace[i].question.feedback) } + let isCorrect = (ans == correctAns); + rb.logBookEvent({"event": "codelensq", "act": `answer:${ans}:${isCorrect}`, "answer":ans, "correct": isCorrect, "div_id": divId}) } diff --git a/runestone/codelens/visualizer.py b/runestone/codelens/visualizer.py index f98cfc86a..dc80afec8 100644 --- a/runestone/codelens/visualizer.py +++ b/runestone/codelens/visualizer.py @@ -22,7 +22,7 @@ from .pg_logger import exec_script_str_local import json import six -from runestone.server.componentdb import addQuestionToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB from runestone.common.runestonedirective import RunestoneDirective def setup(app): @@ -38,6 +38,7 @@ def setup(app): VIS = ''' +

    %(caption)s (%(divid)s)

    @@ -109,6 +110,7 @@ def setup(app): } }); +
    ''' @@ -233,6 +235,9 @@ def js_var_finalizer(input_code, output_trace): res += QUESTION if 'tracedata' in self.options: res += DATA + else: + res += '
    ' + addHTMLToDB(self.options['divid'], self.options['basecourse'], res % self.options) return [nodes.raw('', res % self.options, format='html')] def inject_questions(self, curTrace): diff --git a/runestone/common/js/runestonebase.js b/runestone/common/js/runestonebase.js index 861bd997e..3241a1095 100644 --- a/runestone/common/js/runestonebase.js +++ b/runestone/common/js/runestonebase.js @@ -1,7 +1,35 @@ +/** + * Runestone Base Class + * All runestone components should inherit from RunestoneBase + * + * In addition all runestone components should do the following things: + * 1. Ensure that they are wrapped in a div with the class runestone + * 2. Write their source AND their generated html to the database if the database is configured + * 3. properly save and restore their answers using the checkServer mechanism in this base class. + * Each component must provide an implementation of + * - checkLocalStorage + * - setLocalStorage + * - restoreAnswers + * + * 4. provide a Selenium based unit test + * + **/ + function RunestoneBase () { // Basic parent stuff } +RunestoneBase.prototype.init = function(opts) { + + this.sid = opts.sid; + this.graderactive = opts.graderactive; + + if (opts.enforceDeadline) { + this.deadline = opts.deadline; + } + +}; + RunestoneBase.prototype.logBookEvent = function (eventInfo) { eventInfo.course = eBookConfig.course; if (eBookConfig.useRunestoneServices && eBookConfig.logLevel > 0) { @@ -29,7 +57,7 @@ RunestoneBase.prototype.logRunEvent = function (eventInfo) { RunestoneBase.prototype.checkServer = function (eventInfo) { // Check if the server has stored answer if (this.useRunestoneServices || this.graderactive) { - var data = {}; + let data = {}; data.div_id = this.divid; data.course = eBookConfig.course; data.event = eventInfo; @@ -54,12 +82,12 @@ RunestoneBase.prototype.repopulateFromStorage = function (data, status, whatever RunestoneBase.prototype.shouldUseServer = function (data) { // returns true if server data is more recent than local storage or if server storage is correct - if (data.correct == "T" || localStorage.length === 0) + if (data.correct === "T" || localStorage.length === 0) return true; - var ex = localStorage.getItem(eBookConfig.email + ":" + this.divid + "-given"); + let ex = localStorage.getItem(eBookConfig.email + ":" + this.divid + "-given"); if (ex === null) return true; - var storedData; + let storedData; try { storedData = JSON.parse(ex); } catch (err){ @@ -71,9 +99,10 @@ RunestoneBase.prototype.shouldUseServer = function (data) { } if (data.answer == storedData.answer) return true; - var storageDate = new Date(storedData.timestamp); - var serverDate = new Date(data.timestamp); - if (serverDate < storageDate) - return false; - return true; + let storageDate = new Date(storedData.timestamp); + let serverDate = new Date(data.timestamp); + + return serverDate >= storageDate; + }; + diff --git a/runestone/common/project_template/conf.tmpl b/runestone/common/project_template/conf.tmpl index 2ee49dfe3..c4c271098 100644 --- a/runestone/common/project_template/conf.tmpl +++ b/runestone/common/project_template/conf.tmpl @@ -92,10 +92,20 @@ pygments_style = 'sphinx' # `keep_warnings `_: # If true, keep warnings as “system message” paragraphs in the built documents. -# Regardless of this setting, warnings are always written to the standard error +# Regardless of this setting, warnings are always written to the standard error # stream when sphinx-build is run. keep_warnings = True +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/common/project_template/pavement.tmpl b/runestone/common/project_template/pavement.tmpl index 80022ef0e..6fe7ea161 100644 --- a/runestone/common/project_template/pavement.tmpl +++ b/runestone/common/project_template/pavement.tmpl @@ -40,7 +40,7 @@ options( version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/datafile/__init__.py b/runestone/datafile/__init__.py index 70b96a077..4ca750c5b 100644 --- a/runestone/datafile/__init__.py +++ b/runestone/datafile/__init__.py @@ -21,7 +21,7 @@ from docutils.parsers.rst import directives from docutils.parsers.rst import Directive from sqlalchemy import create_engine, Table, MetaData, select, delete -from runestone.server import get_dburl +from runestone.server.componentdb import engine, meta from runestone.common.runestonedirective import RunestoneDirective def setup(app): @@ -147,9 +147,7 @@ def run(self): else: self.options['edit'] = "false" - try: - engine = create_engine(get_dburl(locals())) - meta = MetaData() + if engine: Source_code = Table('source_code', meta, autoload=True, autoload_with=engine) course_name = env.config.html_context['course_id'] divid = self.options['divid'] @@ -160,8 +158,7 @@ def run(self): course_id = course_name, main_code= source, )) - except Exception as e: - print("the error is ", e) + else: print("Unable to save to source_code table in datafile__init__.py. Possible problems:") print(" 1. dburl or course_id are not set in conf.py for your book") print(" 2. unable to connect to the database using dburl") diff --git a/runestone/dragndrop/dragndrop.py b/runestone/dragndrop/dragndrop.py index 523900de8..b12aef63d 100644 --- a/runestone/dragndrop/dragndrop.py +++ b/runestone/dragndrop/dragndrop.py @@ -19,7 +19,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive -from runestone.server.componentdb import addQuestionToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB from runestone.common.runestonedirective import RunestoneDirective def setup(app): @@ -41,9 +41,8 @@ def setup(app): TEMPLATE_OPTION = """
  • %(dragText)s
  • %(dropText)s
  • -
    """ -TEMPLATE_END = """
""" +TEMPLATE_END = """
""" class DragNDropNode(nodes.General, nodes.Element): @@ -62,6 +61,9 @@ def __init__(self,content): def visit_dnd_node(self,node): res = TEMPLATE_START + node.delimiter = "_start__{}_".format(node.dnd_options['divid']) + self.body.append(node.delimiter) + if "feedback" in node.dnd_options: node.dnd_options["feedback"] = "" + node.dnd_options["feedback"] + "" else: @@ -87,6 +89,12 @@ def depart_dnd_node(self,node): res += node.template_end % node.dnd_options self.body.append(res) + addHTMLToDB(node.dnd_options['divid'], + node.dnd_options['basecourse'], + "".join(self.body[self.body.index(node.delimiter) + 1:])) + + self.body.remove(node.delimiter) + class DragNDrop(RunestoneDirective): """ diff --git a/runestone/dragndrop/test/__init__.py b/runestone/dragndrop/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/dragndrop/test/build_info b/runestone/dragndrop/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/dragndrop/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/dragndrop/test/conf.py b/runestone/dragndrop/test/conf.py index 767bb064f..b6c5deba4 100644 --- a/runestone/dragndrop/test/conf.py +++ b/runestone/dragndrop/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/dragndrop/test/pavement.py b/runestone/dragndrop/test/pavement.py index 6f8e27023..d071fe77d 100644 --- a/runestone/dragndrop/test/pavement.py +++ b/runestone/dragndrop/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/dragndrop/test/test_dragndrop.py b/runestone/dragndrop/test/test_dragndrop.py index 77739c668..5bc1efa4c 100644 --- a/runestone/dragndrop/test/test_dragndrop.py +++ b/runestone/dragndrop/test/test_dragndrop.py @@ -10,21 +10,14 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' +setUpModule, tearDownModule = module_fixture_maker(__file__) jquery_url = "http://code.jquery.com/jquery-1.12.4.min.js" -class DragAndDropQuestion_Tests(unittest.TestCase): +class DragAndDropQuestion_Tests(RunestoneTestCase): def setUp(self): - #self.driver = webdriver.Chrome() - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT + super(DragAndDropQuestion_Tests, self).setUp() self.driver.set_script_timeout(5) with open("jquery_load_helper.js") as f: self.load_jquery_js = f.read() @@ -32,7 +25,6 @@ def setUp(self): with open("drag_and_drop_helper.js") as f: self.js = f.read() - def test_dnd1(self): '''No selection. Button clicked''' self.driver.get(self.host + "/index.html") @@ -140,13 +132,3 @@ def test_dnd4(self): self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-success", cnamestr) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/external/external.py b/runestone/external/external.py index 8ba2ab76d..15b95dcc1 100644 --- a/runestone/external/external.py +++ b/runestone/external/external.py @@ -19,7 +19,6 @@ from runestone.common.runestonedirective import RunestoneDirective from docutils.parsers.rst import Directive from sqlalchemy import create_engine, Table, MetaData, select, delete -from runestone.server import get_dburl from runestone.server.componentdb import addQuestionToDB, addHTMLToDB from runestone.common.runestonedirective import RunestoneDirective @@ -29,7 +28,7 @@ from cgi import escape # py2 __author__ = 'jczetta' -# Code template is directly from question.py at the moment, which is (c) Bradley N. Miller. +# Code template is directly from question.py at the moment, which is (c) Bradley N. Miller. #This is intended as the basis for a potential new gradeable directive class, still potential TODO. diff --git a/runestone/fitb/fitb.py b/runestone/fitb/fitb.py index dd86606f0..b3298e032 100644 --- a/runestone/fitb/fitb.py +++ b/runestone/fitb/fitb.py @@ -15,24 +15,24 @@ # __author__ = 'isaiahmayerchak' +import json +import ast +from numbers import Number from docutils import nodes from docutils.parsers.rst import directives -from docutils.parsers.rst import Directive -#from runestone.assess.assessbase import * -import json -import random -from runestone.server.componentdb import addQuestionToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB from runestone.common import RunestoneDirective def setup(app): app.add_directive('fillintheblank', FillInTheBlank) - app.add_directive('blank', Blank) + app.add_role('blank', BlankRole) app.add_stylesheet('fitb.css') app.add_javascript('fitb.js') app.add_javascript('timedfitb.js') app.add_node(FITBNode, html=(visit_fitb_node, depart_fitb_node)) app.add_node(BlankNode, html=(visit_blank_node, depart_blank_node)) + app.add_node(FITBFeedbackNode, html=(visit_fitb_feedback_node, depart_fitb_feedback_node)) class FITBNode(nodes.General, nodes.Element): @@ -45,42 +45,61 @@ def __init__(self,content): """ super(FITBNode,self).__init__() self.fitb_options = content + # Create a data structure of feedback. + self.feedbackArray = [] def visit_fitb_node(self,node): - res = "" - if 'casei' in node.fitb_options: - node.fitb_options['casei'] = 'true' - else: - node.fitb_options['casei'] = 'false' - res = node.template_start % node.fitb_options + node.delimiter = "_start__{}_".format(node.fitb_options['divid']) + self.body.append(node.delimiter) + res = node.template_start % node.fitb_options self.body.append(res) -def depart_fitb_node(self,node): - res = "" - - res += node.template_end % node.fitb_options +def depart_fitb_node(self, node): + # If there were fewer blanks than feedback items, add blanks at the end of the question. + blankCount = 0 + for _ in node.traverse(BlankNode): + blankCount += 1 + while blankCount < len(node.feedbackArray): + visit_blank_node(self, None) + blankCount += 1 + + # Warn if there are fewer feedback items than blanks. + # TODO: node.source, node.line aren't defined. + #print(node.source, node.line) + if len(node.feedbackArray) < blankCount: + print('Warning at {} line {}: there'' not enough feedback for the number of blanks supplied.'.format(node.source, node.line)) + + # Generate the HTML. + node.fitb_options['json'] = json.dumps(node.feedbackArray) + res = node.template_end % node.fitb_options self.body.append(res) + # add HTML to the Database and clean up + addHTMLToDB(node.fitb_options['divid'], + node.fitb_options['basecourse'], + "".join(self.body[self.body.index(node.delimiter) + 1:])) -class FillInTheBlank(RunestoneDirective): - """ - .. fillintheblank:: fill1412 - - .. blank:: blank1345 - :correct: \\bred\\b - :feedback1: (".*", "Try 'red'") - - Fill in the blanks to make the following sentence: "The red car drove away" The [blank here] + self.body.remove(node.delimiter) - .. blank:: blank52532 - :correct: \\baway\\b - :feedback1: (".*", "Try 'away'") - car drove [blank here] +class FillInTheBlank(RunestoneDirective): + """ + .. fillintheblank:: some_unique_id_here + + Complete the sentence: |blank| had a |blank| lamb. One plus one is: (note that if there aren't enough blanks for the feedback given, they're added to the end of the problem. So, we don't **need** to specify a blank here.) + + - :Mary: Is the correct answer. + :Sue: Is wrong. + :x: Try again. (Note: the last item of feedback matches anything, regardless of the string it's given.) + - :little: That's right. + :.*: Nope. + - :2: Right on! Numbers can be given in decimal, hex (0x10 == 16), octal (0o10 == 8), binary (0b10 == 2), or using scientific notation (1e1 == 10), both here and by the user when answering the question. + :2 1: Close.... (The second number is a tolerance, so this matches 1 or 3.) + :x: Nope. (As earlier, this matches anything.) """ required_arguments = 1 optional_arguments = 0 @@ -89,7 +108,6 @@ class FillInTheBlank(RunestoneDirective): option_spec = RunestoneDirective.option_spec.copy() option_spec.update( {'blankid':directives.unchanged, - 'iscode':directives.flag, 'casei':directives.flag # case insensitive matching }) @@ -97,20 +115,21 @@ def run(self): """ process the fillintheblank directive and generate html for output. :param self: - :return: - .. fillintheblank:: qname - :iscode: boolean - :casei: Case insensitive boolean + :return: Nodes resulting from this directive. ... """ TEMPLATE_START = '''
-

+

''' TEMPLATE_END = ''' -

+ + +
''' @@ -118,129 +137,172 @@ def run(self): self.options['divid'] = self.arguments[0] + # TODO: How to include self.lineno in the directive? fitbNode = FITBNode(self.options) fitbNode.template_start = TEMPLATE_START fitbNode.template_end = TEMPLATE_END self.state.nested_parse(self.content, self.content_offset, fitbNode) - return [fitbNode] - - - -class BlankNode(nodes.General, nodes.Element): - def __init__(self,content): - """ - - Arguments: - - `self`: - - `content`: - """ - super(BlankNode,self).__init__() - self.blank_options = content - - -def visit_blank_node(self,node): - res = "" - - res = node.template_blank_start % node.blank_options - - self.body.append(res) - - -def depart_blank_node(self,node): - fbl = [] - res = "" - feedCounter = 0 - - for k in sorted(node.blank_options.keys()): - if 'feedback' in k: - feedCounter += 1 - node.blank_options['feedLabel'] = "feedback" + str(feedCounter) - pair = eval(node.blank_options[k]) - p0 = pair[0] - p1 = pair[1] - node.blank_options['feedExp'] = p0 - node.blank_options['feedText'] = p1 - res += node.template_blank_option % node.blank_options - - node.blank_options['fbl'] = json.dumps(fbl).replace('"',"'") + # Expected _`structure`, with assigned variable names and transformations made: + # + # .. code-block:: + # :number-lines: + # + # fitbNode = FITBNode() + # Item 1 of problem text + # ... + # Item n of problem text + # feedback_bullet_list = bullet_list() <-- The last element in fitbNode. + # feedback_list_item = list_item() <-- Feedback for the first blank. + # feedback_field_list = field_list() + # feedback_field = field() + # feedback_field_name = field_name() <-- Contains an answer. + # feedback_field_body = field_body() <-- Contains feedback for this answer. + # feedback_field = field() <-- Another answer/feedback pair. + # feedback_list_item = bullet_item() <-- Feedback for the second blank. + # ...etc. ... + # + # This becomes a data structure: + # + # .. code-block:: + # :number-lines: + # + # self.feedbackArray = [ + # [ # blankArray + # { # blankFeedbackDict: feedback 1 + # "regex" : feedback_field_name # (An answer, as a regex; + # "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR + # "number" : [min, max] # a range of correct numeric answers. + # "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer. + # }, + # { # Feedback 2 + # Same as above. + # } + # ], + # [ # Blank 2, same as above. + # ] + # ] + # + # ...and a transformed node structure: + # + # .. code-block:: + # :number-lines: + # + # fitbNode = FITBNode() + # Item 1 of problem text + # ... + # Item n of problem text + # FITBFeedbackNode(), which contains all the nodes in blank 1's feedback_field_body + # ... + # FITBFeedbackNode(), which contains all the nodes in blank n's feedback_field_body + # + feedback_bullet_list = fitbNode.pop() + if not isinstance(feedback_bullet_list, nodes.bullet_list): + self.error('The last item in a fill-in-the-blank question must be a bulleted list.') + for feedback_list_item in feedback_bullet_list.children: + assert isinstance(feedback_list_item, nodes.list_item) + feedback_field_list = feedback_list_item[0] + if len(feedback_list_item) != 1 or not isinstance(feedback_field_list, nodes.field_list): + self.error('Each list item in a fill-in-the-blank problems must contain only one item, a field list.') + blankArray = [] + for feedback_field in feedback_field_list: + assert isinstance(feedback_field, nodes.field) + + feedback_field_name = feedback_field[0] + assert isinstance(feedback_field_name, nodes.field_name) + feedback_field_name_raw = feedback_field_name.rawsource + # See if this is a number, optinonally followed by a tolerance. + try: + # Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``. + tmp = feedback_field_name_raw.split() + str_num = tmp[0] + list_tol = tmp[1:] + num = ast.literal_eval(str_num) + assert isinstance(num, Number) + # If no tolerance is given, use a tolarance of 0. + if len(list_tol) == 0: + tol = 0 + else: + assert len(list_tol) == 1 + tol = ast.literal_eval(list_tol[0]) + assert isinstance(tol, Number) + # We have the number and a tolerance. Save that. + blankFeedbackDict = {"number": [num - tol, num + tol]} + except (SyntaxError, ValueError, AssertionError): + # We can't parse this as a number, so assume it's a regex. + blankFeedbackDict = { + 'regex': + # The given regex must match the entire string, from the beginning (which may be preceeded by spaces) ... + '^ *' + + # ... to the contents (where a single space in the provided pattern is treated as one or more spaces in the student's anwer) ... + feedback_field_name.rawsource.replace(' ', ' +') + # ... to the end (also with optional whitespace). + + ' *$', + 'regexFlags': + 'i' if 'casei' in self.options else '', + } + blankArray.append(blankFeedbackDict) + + feedback_field_body = feedback_field[1] + assert isinstance(feedback_field_body, nodes.field_body) + # Append feedback for this asnwer to the end of the fitbNode. + ffn = FITBFeedbackNode(feedback_field_body.rawsource, *feedback_field_body.children, **feedback_field_body.attributes) + ffn.blankFeedbackDict = blankFeedbackDict + fitbNode += ffn + + # Add all the feedback for this blank to the feedbackArray. + fitbNode.feedbackArray.append(blankArray) - res += node.template_option_end % node.blank_options - - - self.body.append(res) - - - - -class Blank(RunestoneDirective): - """ -.. blank:: blank52532 - :correct: \\baway\\b - :feedback1: (".*", "Try 'away'") - - car drove [the blank will be here] - - """ - required_arguments = 1 - optional_arguments = 0 - final_argument_whitespace = True - has_content = True - option_spec = {'correct':directives.unchanged, - 'feedback1':directives.unchanged, - 'feedback2':directives.unchanged, - 'feedback3':directives.unchanged, - 'feedback4':directives.unchanged, - } - - def run(self): - """ - process the fillintheblank directive and generate html for output. - :param self: - :return: - .. blank:: qname - :correct: regular expression - :feedback1: ('.*', 'this is the message') - :feedback2: (RegEx, MessageString) - :feedback3: (RegEx, MessageString) - :feedback4: (RegEx, MessageString) - - - - Question text - ... - """ - - self.options['divid'] = self.arguments[0] - if self.content: - if 'iscode' in self.options: - self.options['bodytext'] = '
' + "\n".join(self.content) + '
' - else: - self.options['bodytext'] = "\n".join(self.content) - else: - self.options['bodytext'] = '\n' - - if 'correct' not in self.options: - raise ValueError("missing correct value in %s"%self.options['divid']) - - TEMPLATE_BLANK_START = ''' - - ''' - TEMPLATE_BLANK_OPTION = ''' - - %(feedText)s - ''' - TEMPLATE_BLANK_END = ''' - - - ''' - - blankNode = BlankNode(self.options) - blankNode.template_blank_start = TEMPLATE_BLANK_START - blankNode.template_blank_option = TEMPLATE_BLANK_OPTION - blankNode.template_option_end = TEMPLATE_BLANK_END + return [fitbNode] - self.state.nested_parse(self.content, self.content_offset, blankNode) - return [blankNode] +# BlankRole +# --------- +# Create role representing the blank in a fill-in-the-blank question. This function returns a tuple of two values: +# +# 0. A list of nodes which will be inserted into the document tree at the point where the interpreted role was encountered (can be an empty list). +# #. A list of system messages, which will be inserted into the document tree immediately after the end of the current block (can also be empty). +def BlankRole( + # _`roleName`: the local name of the interpreted role, the role name actually used in the document. + roleName, + # _`rawtext` is a string containing the enitre interpreted text input, including the role and markup. Return it as a problematic node linked to a system message if a problem is encountered. + rawtext, + # The interpreted _`text` content. + text, + # The line number (_`lineno`) where the interpreted text begins. + lineno, + # _`inliner` is the docutils.parsers.rst.states.Inliner object that called this function. It contains the several attributes useful for error reporting and document tree access. + inliner, + # A dictionary of directive _`options` for customization (from the "role" directive), to be interpreted by this function. Used for additional attributes for the generated elements and other functionality. + options={}, + # A list of strings, the directive _`content` for customization (from the "role" directive). To be interpreted by the role function. + content=[]): + + # Blanks ignore all arguments, just inserting a blank. + return [BlankNode(rawtext)], [] + +class BlankNode(nodes.Inline, nodes.TextElement): + pass + +def visit_blank_node(self, node): + self.body.append('') + +def depart_blank_node(self, node): + pass + + +# Contains feedback for one answer. +class FITBFeedbackNode(nodes.General, nodes.Element): + pass + +def visit_fitb_feedback_node(self, node): + # Save the HTML generated thus far. Anything generated under this node will be placed in JSON. + self.context.append(self.body) + self.body = [] + +def depart_fitb_feedback_node(self, node): + # Place all the HTML generated for this node and its children into the feedbackArray. + node.blankFeedbackDict['feedback'] = ''.join(self.body) + # Restore HTML generated thus far. + self.body = self.context.pop() diff --git a/runestone/fitb/js/fitb.js b/runestone/fitb/js/fitb.js index b5f786362..61bbf7076 100644 --- a/runestone/fitb/js/fitb.js +++ b/runestone/fitb/js/fitb.js @@ -28,95 +28,25 @@ FITB.prototype = new RunestoneBase(); FITB.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; // entire

element this.useRunestoneServices = opts.useRunestoneServices; this.origElem = orig; this.divid = orig.id; - this.questionArray = []; this.correct = null; - this.feedbackArray = []; - /* this.feedbackArray is an array of array of arrays--each outside element is a blank. Each middle element is a different "incorrect" feedback - that is tailored for how the question is incorrectly answered. Each inside array contains 2 elements: the regular expression, then text */ - this.children = []; // this contains all of the child elements of the entire tag... - this.correctAnswerArray = []; // This array contains the regular expressions of the correct answers - - this.adoptChildren(); - this.populateCorrectAnswerArray(); - this.populateQuestionArray(); - - this.casei = false; // Case insensitive--boolean - if ($(this.origElem).data("casei") === true) { - this.casei = true; - } - this.populateFeedbackArray(); + // See comments in fitb.py for the format of ``feedbackArray`` (which is identical in both files). + // + // Find the script tag containing JSON and parse it. See `SO `_. + this.feedbackArray = JSON.parse(this.scriptSelector(this.origElem).html()); + this.createFITBElement(); this.checkServer("fillb"); }; -/*==================================== -==== Functions parsing data ==== -==== out of intermediate HTML ==== -====================================*/ - -FITB.prototype.adoptChildren = function () { - // populates this.children - var children = this.origElem.childNodes; - for (var i = 0; i < this.origElem.childNodes.length; i++) { - if ($(this.origElem.childNodes[i]).is("[data-blank]")) { - this.children.push(this.origElem.childNodes[i]); - } - } -}; - -FITB.prototype.populateCorrectAnswerArray = function () { - for (var i = 0; i < this.children.length; i++) { - for (var j=0; j < this.children[i].childNodes.length; j++) { - if ($(this.children[i].childNodes[j]).is("[data-answer]")) { - this.correctAnswerArray.push($([this.children[i].childNodes[j]]).text().replace(/\\\\/g,"\\")); - } - } - } -}; - -FITB.prototype.populateQuestionArray = function () { - for (var i = 0; i < this.children.length; i++) { - for (var j = 0; j < this.children[i].childNodes.length; j++) { - if ($(this.children[i].childNodes[j]).is("[data-answer]")) { - var delimiter = this.children[i].childNodes[j].outerHTML; - - var fulltext = $(this.children[i]).html(); - var temp = fulltext.split(delimiter); - this.questionArray.push(temp[0]); - break; - } - } - } -}; - -FITB.prototype.populateFeedbackArray = function () { - for (var i = 0; i < this.children.length; i++) { - var AnswerNodeList = []; - var tmpContainArr = []; - for (var j = 0; j < this.children[i].childNodes.length; j++) { - if ($(this.children[i].childNodes[j]).is("[data-feedback=text]")) { - - AnswerNodeList.push(this.children[i].childNodes[j]); - for (var k = 0; k < this.children[i].childNodes.length; k++) { - if ($(this.children[i].childNodes[k]).is("[data-feedback=regex]")) { - if ($(this.children[i].childNodes[j]).attr("for") === this.children[i].childNodes[k].id) { - var tempArr = []; - tempArr.push(this.children[i].childNodes[k].innerHTML.replace(/\\\\/g, "\\")); - tempArr.push(this.children[i].childNodes[j].innerHTML); - tmpContainArr.push(tempArr); - break; - } - } - } - } - } - this.feedbackArray.push(tmpContainArr); - } -}; +// Find the script tag containing JSON in a given root DOM node. +FITB.prototype.scriptSelector = function (root_node) { + return $(root_node).find('script[type="application/json"]'); +} /*=========================================== ==== Functions generating final HTML ==== @@ -138,22 +68,14 @@ FITB.prototype.renderFITBInput = function () { $(this.containerDiv).addClass("alert alert-warning"); this.containerDiv.id = this.divid; - this.blankArray = []; - for (var i = 0; i < this.children.length; i++) { - var question = document.createElement("span"); - question.innerHTML = this.questionArray[i]; - this.containerDiv.appendChild(question); - - var blank = document.createElement("input"); - $(blank).attr({ - "type": "text", - "id": this.divid + "_blank" + i, - "class": "form form-control selectwidthauto" - }); - this.containerDiv.appendChild(blank); - this.blankArray.push(blank); - } - + // Copy the original elements to the container holding what the user will see. + $(this.origElem).children().clone().appendTo(this.containerDiv); + // Remove the script tag. + this.scriptSelector(this.containerDiv).remove(); + // Set the class for the text inputs, then store references to them. + let ba = $(this.containerDiv).find(':input'); + ba.attr('class', 'form form-control selectwidthauto'); + this.blankArray = ba.toArray(); }; FITB.prototype.renderFITBButtons = function () { @@ -268,24 +190,45 @@ FITB.prototype.startEvaluation = function (logFlag) { }; FITB.prototype.evaluateAnswers = function () { - for (var i = 0; i < this.children.length; i++) { + for (var i = 0; i < this.blankArray.length; i++) { var given = this.blankArray[i].value; - var modifiers = ""; - if (this.casei) { - modifiers = "i"; - } - var patt = RegExp(this.correctAnswerArray[i], modifiers); - if (given !== "") { - this.isCorrectArray.push(patt.test(given)); - } else { + // If this blank is empty, provide no feedback for it. + if (given === "") { this.isCorrectArray.push(""); - } - - if (!this.isCorrectArray[i]) { - this.populateDisplayFeed(i, given); + } else { + // Look through all feedback for this blank. The last element in the array always matches. + var fbl = this.feedbackArray[i]; + for (var j = 0; j < fbl.length; j++) { + // The last item of feedback always matches. + if (j === fbl.length - 1) { + this.displayFeed.push(fbl[j]['feedback']); + break; + } + // If this is a regexp... + if ('regex' in fbl[j]) { + var patt = RegExp(fbl[j]['regex'], fbl[j]['regexFlags']); + if (patt.test(given)) { + this.displayFeed.push(fbl[j]['feedback']); + break; + } + } else { + // This is a number. + console.assert('number' in fbl[j]); + var [min, max] = fbl[j]['number']; + // Convert the given string to a number. While there are `lots of ways `_ to do this,, this version supports other bases (hex/binary/octal) as well as floats. + var actual = +given; + if (actual >= min && actual <= max) { + this.displayFeed.push(fbl[j]['feedback']); + break; + } + } + } + // The answer is correct if it matched the first element in the array. + this.isCorrectArray.push(j === 0); } } + if ($.inArray("", this.isCorrectArray) < 0 && $.inArray(false, this.isCorrectArray) < 0) { this.correct = true; } else if (this.isCompletelyBlank()) { @@ -306,22 +249,9 @@ FITB.prototype.isCompletelyBlank = function () { return true; }; -FITB.prototype.populateDisplayFeed = function (index, given) { - var fbl = this.feedbackArray[index]; - for (var j = 0; j < fbl.length; j++) { - for (var k = 0; k < fbl[j].length; k++) { - var patt = RegExp(fbl[j][k]); - if (patt.test(given)) { - this.displayFeed.push(fbl[j][1]); - return 0; - } - } - } -}; - FITB.prototype.renderFITBFeedback = function () { if (this.correct) { - $(this.feedBackDiv).html("You are Correct!"); + $(this.feedBackDiv).html("Correct.
"); $(this.feedBackDiv).attr("class", "alert alert-success"); for (var j = 0; j < this.blankArray.length; j++) { $(this.blankArray[j]).removeClass("input-validation-error"); @@ -330,7 +260,7 @@ FITB.prototype.renderFITBFeedback = function () { if (this.displayFeed === null) { this.displayFeed = ""; } - $(this.feedBackDiv).html("Incorrect. "); + $(this.feedBackDiv).html("Incorrect.
"); for (var j = 0; j < this.blankArray.length; j++) { if (!this.isCorrectArray[j]) { $(this.blankArray[j]).addClass("input-validation-error"); @@ -338,12 +268,12 @@ FITB.prototype.renderFITBFeedback = function () { $(this.blankArray[j]).removeClass("input-validation-error"); } } - for (var i = 0; i < this.displayFeed.length; i++) { - this.feedBackDiv.innerHTML += this.displayFeed[i]; - this.feedBackDiv.appendChild(document.createElement("br")); - } $(this.feedBackDiv).attr("class", "alert alert-danger"); } + for (var i = 0; i < this.displayFeed.length; i++) { + this.feedBackDiv.innerHTML += this.displayFeed[i]; + this.feedBackDiv.appendChild(document.createElement("br")); + } }; /*================================== diff --git a/runestone/fitb/test/__init__.py b/runestone/fitb/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/fitb/test/_sources/index.rst b/runestone/fitb/test/_sources/index.rst index d68d51400..1a9f31bdb 100644 --- a/runestone/fitb/test/_sources/index.rst +++ b/runestone/fitb/test/_sources/index.rst @@ -9,14 +9,11 @@ Fill in the Blank .. fillintheblank:: fill1412 - .. blank:: blank1345 - :correct: \\bred\\b - :feedback1: (".*", "Try 'red'") + Fill in the blanks to make the following sentence: "The red car drove away." - Fill in the blanks to make the following sentence: "The red car drove away" The + The |blank| car drove |blank|. - .. blank:: blank52532 - :correct: \\baway\\b - :feedback1: (".*", "Try 'away'") - - car drove \ No newline at end of file + - :red: Correct. + :x: Try 'red'. + - :car: Correct. + :x: Try 'away'. diff --git a/runestone/fitb/test/build_info b/runestone/fitb/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/fitb/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/fitb/test/conf.py b/runestone/fitb/test/conf.py index d54c3be68..44d6b97fc 100644 --- a/runestone/fitb/test/conf.py +++ b/runestone/fitb/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/fitb/test/pavement.py b/runestone/fitb/test/pavement.py index 887306b7c..c887abcda 100644 --- a/runestone/fitb/test/pavement.py +++ b/runestone/fitb/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/fitb/test/test_fitb.py b/runestone/fitb/test/test_fitb.py index e055f9d90..754c20af1 100644 --- a/runestone/fitb/test/test_fitb.py +++ b/runestone/fitb/test/test_fitb.py @@ -1,80 +1,72 @@ -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' +setUpModule, tearDownModule = module_fixture_maker(__file__) -class FITBtests(unittest.TestCase): +class FITBtests(RunestoneTestCase): + ## Helpers + ## ======= def setUp(self): - #self.driver = webdriver.Firefox() # good for development - # self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.driver = webdriver.PhantomJS() - self.host = 'http://127.0.0.1:' + PORT + super(FITBtests, self).setUp() + self.driver.get(self.host + "/index.html") # Access page + + # Return the DIV containing a FITB question. + def find_fitb(self): + self.fitb = self.driver.find_element_by_id("fill1412") + return self.fitb + + # Find one of the blanks, based on the given index. + def find_blank(self, index): + return self.fitb.find_elements_by_tag_name("input")[index] + + # Click the "Check me" button. + def click_checkme(self): + self.fitb.find_element_by_tag_name('button').click() + + # Find the question's feedback element. + def find_feedback(self): + return self.fitb.find_element_by_id("fill1412_feedback") + ## Tests + ## ===== # One of two correct answers def test_fitb(self): ''' http://runestoneinteractive.org/build/html/directives.html#fill-in-the-blank for documentation ''' - self.driver.get(self.host + "/index.html") # Access page - quest = self.driver.find_element_by_id("fill-in-the-blank") - blank1 = quest.find_element_by_id("fill1412_blank0") - blank1.send_keys("red") - # inp = blank1.get_attribute("input") - checkme = quest.find_element_by_tag_name('button') - checkme.click() - feedback = quest.find_element_by_id("fill1412_feedback") + self.find_fitb() + self.find_blank(0).send_keys("red") + self.click_checkme() + feedback = self.find_feedback() self.assertIsNotNone(feedback.text) # No answers yet -- Incorrect feedback def test_fitb2(self): - self.driver.get(self.host + "/index.html") # Access page - quest = self.driver.find_element_by_id("fill-in-the-blank") - blank1 = quest.find_element_by_id("fill1412_blank0") - checkme = quest.find_element_by_tag_name('button') - checkme.click() - feedback = quest.find_element_by_id("fill1412_feedback") + self.find_fitb() + self.click_checkme() + feedback = self.find_feedback() + self.assertIsNotNone(feedback.text) self.assertIn("Incorrect",feedback.text) - # Both correct answers + # Both correct answers def test_fitb3(self): - self.driver.get(self.host + "/index.html") # Access page - quest = self.driver.find_element_by_id("fill-in-the-blank") - - blank1 = quest.find_element_by_id("fill1412_blank0") - blank2 = quest.find_element_by_id("fill1412_blank1") - blank1.send_keys("red") - blank2.send_keys("away") - checkme = quest.find_element_by_tag_name('button') - checkme.click() - feedback = quest.find_element_by_id("fill1412_feedback") + self.find_fitb() + self.find_blank(0).send_keys("red") + self.find_blank(1).send_keys("away") + self.click_checkme() + feedback = self.find_feedback() self.assertIn("Correct", feedback.text) def test_fitb4(self): - self.driver.get(self.host + "/index.html") # Access page - quest = self.driver.find_element_by_id("fill-in-the-blank") - blank1 = quest.find_element_by_id("fill1412_blank0") - blank2 = quest.find_element_by_id("fill1412_blank1") - blank1.send_keys("reds") # Type something wrong - blank1.clear() # Delete the wrong thing - blank1.send_keys("red") # Type the right thing - blank2.send_keys("away") # Type another correct answer in another blank - checkme = quest.find_element_by_tag_name('button') - checkme.click() - feedback = quest.find_element_by_id("fill1412_feedback") - self.assertIn("Correct",feedback.text) - - - def tearDown(self): - self.driver.quit() - - - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) + self.find_fitb() + blank0 = self.find_blank(0) + # Type an incorrect answer. + blank0.send_keys("red") + # Delete it. + blank0.clear() + # Type the correct answer. + blank0.send_keys("red") + self.find_blank(1).send_keys("away") + self.click_checkme() + feedback = self.find_feedback() + self.assertIn("Correct", feedback.text) diff --git a/runestone/parsons/js/parsons.js b/runestone/parsons/js/parsons.js index b2c3f04f3..d070bad54 100755 --- a/runestone/parsons/js/parsons.js +++ b/runestone/parsons/js/parsons.js @@ -1023,6 +1023,7 @@ Parsons.counter = 0; // Initialize based on what is specified in the HTML file Parsons.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; // entire

 element that will be replaced by new HTML
 	this.origElem = orig;
 	this.useRunestoneServices = opts.useRunestoneServices;
diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py
index 72f6426ad..f4b087abf 100755
--- a/runestone/parsons/parsons.py
+++ b/runestone/parsons/parsons.py
@@ -20,11 +20,12 @@
 from docutils.parsers.rst import directives
 from docutils.parsers.rst import Directive
 from runestone.assess import Assessment
-from runestone.server.componentdb import addQuestionToDB
+from runestone.server.componentdb import addQuestionToDB, addHTMLToDB
 from runestone.common.runestonedirective import RunestoneDirective
 
 def setup(app):
     app.add_directive('parsonsprob', ParsonsProblem)
+    app.add_node(ParsonsNode, html=(visit_parsons_node, depart_parsons_node))
     app.add_stylesheet('parsons.css')
     app.add_stylesheet('lib/prettify.css')
     app.add_javascript('lib/prettify.js')
@@ -32,6 +33,35 @@ def setup(app):
     app.add_javascript('parsons.js')
     app.add_javascript('timedparsons.js')
 
+
+TEMPLATE = '''
+        
+
+        %(qnumber)s: %(instructions)s%(code)s
+        
+
+ ''' + +class ParsonsNode(nodes.General, nodes.Element): + def __init__(self, options): + super(ParsonsNode, self).__init__() + self.parsonsnode_components = options + +def visit_parsons_node(self, node): + div_id = node.parsonsnode_components['divid'] + components = dict(node.parsonsnode_components) + components.update({'divid': div_id}) + res = TEMPLATE % components + addHTMLToDB(div_id, components['basecourse'], res) + + self.body.append(res) + +def depart_parsons_node(self,node): + pass + + + + class ParsonsProblem(Assessment): """ .. parsonsprob:: unqiue_problem_id_here @@ -108,15 +138,6 @@ def findmax(alist): addQuestionToDB(self) - TEMPLATE = ''' -<<<<<<< HEAD -======= -
->>>>>>> RunestoneInteractive/master -
-        %(qnumber)s: %(instructions)s%(code)s
-        
- ''' self.options['divid'] = self.arguments[0] self.options['qnumber'] = self.getNumber() self.options['instructions'] = "" @@ -159,4 +180,5 @@ def findmax(alist): self.options['divid'] = self.arguments[0] self.assert_has_content() - return [nodes.raw('', TEMPLATE % self.options, format='html')] + + return [ParsonsNode(self.options)] diff --git a/runestone/poll/js/poll.js b/runestone/poll/js/poll.js index 89dce1d86..342363822 100644 --- a/runestone/poll/js/poll.js +++ b/runestone/poll/js/poll.js @@ -14,6 +14,7 @@ function Poll(opts) { Poll.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; //entire

element this.origElem = orig; this.divid = orig.id; diff --git a/runestone/poll/test/__init__.py b/runestone/poll/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/poll/test/build_info b/runestone/poll/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/poll/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/poll/test/conf.py b/runestone/poll/test/conf.py index f7f41a42a..dba7d3935 100644 --- a/runestone/poll/test/conf.py +++ b/runestone/poll/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/poll/test/pavement.py b/runestone/poll/test/pavement.py index 492b67b7a..02db28672 100644 --- a/runestone/poll/test/pavement.py +++ b/runestone/poll/test/pavement.py @@ -36,7 +36,7 @@ ) ) -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/poll/test/test_poll.py b/runestone/poll/test/test_poll.py index c867aac55..cdae659ba 100644 --- a/runestone/poll/test/test_poll.py +++ b/runestone/poll/test/test_poll.py @@ -1,20 +1,8 @@ -from selenium import webdriver -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' +setUpModule, tearDownModule = module_fixture_maker(__file__) -class PollTests(unittest.TestCase): - def setUp(self): - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT - - - def tearDown(self): - self.driver.quit() - - - ################################################################################################# +class PollTests(RunestoneTestCase): def test_poll(self): ''' test the poll directive ''' self.driver.get(self.host + '/index.html') @@ -35,10 +23,3 @@ def test_poll(self): # just make sure we can find the results div - an exception will be raised if the div cannot be found poll_div.find_element_by_id('pollid1_results') - - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main() \ No newline at end of file diff --git a/runestone/question/test/build_info b/runestone/question/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/question/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/question/test/conf.py b/runestone/question/test/conf.py index 6721f5064..814a802c9 100644 --- a/runestone/question/test/conf.py +++ b/runestone/question/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/question/test/pavement.py b/runestone/question/test/pavement.py index bc9589d88..3d4005c05 100644 --- a/runestone/question/test/pavement.py +++ b/runestone/question/test/pavement.py @@ -36,7 +36,7 @@ ) ) -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/question/test/test_question.py b/runestone/question/test/test_question.py index 222c05c93..86dc6ecbe 100644 --- a/runestone/question/test/test_question.py +++ b/runestone/question/test/test_question.py @@ -1,17 +1,8 @@ -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' - -class QuestionTests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT +setUpModule, tearDownModule = module_fixture_maker(__file__) +class QuestionTests(RunestoneTestCase): def test_hello(self): ''' 1. Get the outer div id of the activecode component @@ -68,15 +59,3 @@ def test_mc2(self): cnamestr = fb.get_attribute("class") self.assertEqual(cnamestr, "alert alert-success") - - - def tearDown(self): - self.driver.quit() - - - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main() \ No newline at end of file diff --git a/runestone/reveal/test/__init__.py b/runestone/reveal/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/reveal/test/build_info b/runestone/reveal/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/reveal/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/reveal/test/conf.py b/runestone/reveal/test/conf.py index 767bb064f..b6c5deba4 100644 --- a/runestone/reveal/test/conf.py +++ b/runestone/reveal/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/reveal/test/pavement.py b/runestone/reveal/test/pavement.py index 9f2fda861..9e7d88637 100644 --- a/runestone/reveal/test/pavement.py +++ b/runestone/reveal/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/reveal/test/test_reveal.py b/runestone/reveal/test/test_reveal.py index 920ebf0f7..2368393e0 100644 --- a/runestone/reveal/test/test_reveal.py +++ b/runestone/reveal/test/test_reveal.py @@ -4,22 +4,11 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase -PORT = '8081' +setUpModule, tearDownModule = module_fixture_maker(__file__) -class RevealQuestion_Tests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Chrome() - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT - - +class RevealQuestion_Tests(RunestoneTestCase): def test_r1(self): '''Initial view. Content is hidden''' self.driver.get(self.host + "/index.html") @@ -58,13 +47,3 @@ def test_r3(self): cnamestr = q1.get_attribute("style") self.assertEqual("display: none;", cnamestr) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/server/__init__.py b/runestone/server/__init__.py index 53c5e6adc..d016ef80c 100644 --- a/runestone/server/__init__.py +++ b/runestone/server/__init__.py @@ -1,4 +1,3 @@ -from .chapternames import * from os import environ import re @@ -14,8 +13,8 @@ def get_dburl(outer={}): # outer may contain the locals from the calling function # nonlocal env, settings # Python 3 only - if all([x in environ for x in ['DBUSER', 'DBHOST', 'DBNAME']]): - return 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**environ) + if 'DBURL' in environ: + return environ['DBURL'] if 'options' in outer: return outer['options'].build.template_args['dburl'] diff --git a/runestone/server/chapternames.py b/runestone/server/chapternames.py index 44036e527..34f1b52b9 100644 --- a/runestone/server/chapternames.py +++ b/runestone/server/chapternames.py @@ -3,6 +3,7 @@ import re from sqlalchemy import create_engine, Table, MetaData, select, delete +from .componentdb import engine, meta from collections import OrderedDict import sys from functools import reduce @@ -87,26 +88,10 @@ def getTOCEntries(ftext): def addChapterInfoToDB(subChapD, chapTitles, course_id): - dbname = 'runestone' - # Provide a default database URI if the ``USER`` environment variables is define (it is on Linux/Mac). - uname = os.environ.get('USER') - if uname == 'bnmnetp': - uname = 'bnmnetp_courselib' - dbname = 'bnmnetp_courselib' - - dburl = 'postgresql://{}@localhost/{}'.format(uname,dbname) - - - if all(name in os.environ for name in ['DBHOST', 'DBPASS', 'DBUSER', 'DBNAME']): - dburl = 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**os.environ) - - try: - engine = create_engine(dburl) - except ImportError as imperr: + if not engine: print("You need to install a DBAPI module - psycopg2 for Postgres") return - meta = MetaData() chapters = Table('chapters', meta, autoload=True, autoload_with=engine) sub_chapters = Table('sub_chapters', meta, autoload=True, autoload_with=engine) diff --git a/runestone/server/componentdb.py b/runestone/server/componentdb.py index 47010200a..df2b32ff3 100644 --- a/runestone/server/componentdb.py +++ b/runestone/server/componentdb.py @@ -21,21 +21,25 @@ import os from sqlalchemy import create_engine, Table, MetaData, select, delete, update, and_ +from . import get_dburl # create a global DB query engine to share for the rest of the file -if all(name in os.environ for name in ['DBHOST', 'DBPASS', 'DBUSER', 'DBNAME']): - dburl = 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**os.environ) +try: + dburl = get_dburl() engine = create_engine(dburl) +except RuntimeError as e: + dburl = None + engine = None + meta = None + print("Skipping all DB operations because environment variables not set up") +else: + # If no exceptions are raised, then set up the database. meta = MetaData() questions = Table('questions', meta, autoload=True, autoload_with=engine) assignment_types = Table('assignment_types', meta, autoload=True, autoload_with=engine) assignment_questions = Table('assignment_questions', meta, autoload=True, autoload_with=engine) courses = Table('courses', meta, autoload=True, autoload_with=engine) -else: - dburl = None - engine = None - print("Skipping all DB operations because environment variables not set up") def logSource(self): sourcelog = self.state.document.settings.env.config.html_context.get('dsource', None) @@ -51,11 +55,6 @@ def logSource(self): def addQuestionToDB(self): - if all(name in os.environ for name in ['DBHOST', 'DBPASS', 'DBUSER', 'DBNAME']): - dburl = 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**os.environ) - else: - dburl = None - if dburl: basecourse = self.state.document.settings.env.config.html_context.get('basecourse', "unknown") if basecourse == "unknown": @@ -226,11 +225,6 @@ def addAssignmentToDB(name = None, course_id = None, assignment_type_id = None, return a_id def addHTMLToDB(divid, basecourse, htmlsrc): - if all(name in os.environ for name in ['DBHOST', 'DBPASS', 'DBUSER', 'DBNAME']): - dburl = 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**os.environ) - else: - dburl = None - if dburl: last_changed = datetime.now() sel = select([questions]).where(and_(questions.c.name == divid, diff --git a/runestone/shortanswer/js/shortanswer.js b/runestone/shortanswer/js/shortanswer.js index feb2577f6..9e8ea0206 100644 --- a/runestone/shortanswer/js/shortanswer.js +++ b/runestone/shortanswer/js/shortanswer.js @@ -25,13 +25,12 @@ ShortAnswer.prototype = new RunestoneBase(); ========================================*/ ShortAnswer.prototype.init = function (opts) { RunestoneBase.apply(this, arguments); + RunestoneBase.prototype.init.apply(this, arguments); var orig = opts.orig; // entire

element that will be replaced by new HTML this.useRunestoneServices = opts.useRunestoneServices || eBookConfig.useRunestoneServices; this.origElem = orig; this.divid = orig.id; this.question = this.origElem.innerHTML; - this.sid = opts.sid; - this.graderactive = opts.graderactive; this.optional = false; if ($(this.origElem).is("[data-optional]")) { diff --git a/runestone/shortanswer/test/__init__.py b/runestone/shortanswer/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/shortanswer/test/build_info b/runestone/shortanswer/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/shortanswer/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/shortanswer/test/conf.py b/runestone/shortanswer/test/conf.py index 767bb064f..b6c5deba4 100644 --- a/runestone/shortanswer/test/conf.py +++ b/runestone/shortanswer/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/shortanswer/test/pavement.py b/runestone/shortanswer/test/pavement.py index d4786a7a4..d973f12d5 100644 --- a/runestone/shortanswer/test/pavement.py +++ b/runestone/shortanswer/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/shortanswer/test/test_shortanswer.py b/runestone/shortanswer/test/test_shortanswer.py index 0bd3915b0..672dd586c 100644 --- a/runestone/shortanswer/test/test_shortanswer.py +++ b/runestone/shortanswer/test/test_shortanswer.py @@ -4,21 +4,11 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys - -PORT = '8081' - -class ShortAnswerQuestion_Tests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase +setUpModule, tearDownModule = module_fixture_maker(__file__) +class ShortAnswerQuestion_Tests(RunestoneTestCase): def test_sa1(self): '''No input. Button not clicked''' self.driver.get(self.host + "/index.html") @@ -69,20 +59,10 @@ def test_sa4(self): btn_check = t1.find_element_by_tag_name('button') btn_check.click() - + ta.clear() fb = t1.find_element_by_id("question1_feedback") self.assertIsNotNone(fb) cnamestr = fb.get_attribute("class") self.assertIn("alert-success", cnamestr) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/tabbedStuff/test/__init__.py b/runestone/tabbedStuff/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/runestone/tabbedStuff/test/build_info b/runestone/tabbedStuff/test/build_info deleted file mode 100644 index c6bc02df3..000000000 --- a/runestone/tabbedStuff/test/build_info +++ /dev/null @@ -1 +0,0 @@ -2.7.14-46-g2d30eb5 diff --git a/runestone/tabbedStuff/test/conf.py b/runestone/tabbedStuff/test/conf.py index 767bb064f..b6c5deba4 100644 --- a/runestone/tabbedStuff/test/conf.py +++ b/runestone/tabbedStuff/test/conf.py @@ -90,6 +90,17 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# `rst_prolog `_: +# A string of reStructuredText that will be included at the beginning of every +# source file that is read. +rst_prolog = ( +# For fill-in-the-blank questions, provide a convenient means to indicate a blank. +""" + +.. |blank| replace:: :blank:`x` +""" +) + # -- Options for HTML output --------------------------------------------------- diff --git a/runestone/tabbedStuff/test/pavement.py b/runestone/tabbedStuff/test/pavement.py index 3d17fb09b..64dacec54 100644 --- a/runestone/tabbedStuff/test/pavement.py +++ b/runestone/tabbedStuff/test/pavement.py @@ -40,7 +40,7 @@ version = pkg_resources.require("runestone")[0].version options.build.template_args['runestone_version'] = version -# If DBUSER etc. are in the environment override dburl +# If DBURL is in the environment override dburl options.build.template_args['dburl'] = get_dburl(outer=locals()) from runestone import build # build is called implicitly by the paver driver. diff --git a/runestone/tabbedStuff/test/test_tabbedStuff.py b/runestone/tabbedStuff/test/test_tabbedStuff.py index 88ce66122..5895f9754 100644 --- a/runestone/tabbedStuff/test/test_tabbedStuff.py +++ b/runestone/tabbedStuff/test/test_tabbedStuff.py @@ -4,22 +4,11 @@ __author__ = 'yasinovskyy' -from selenium import webdriver -from selenium.webdriver import ActionChains -from selenium.common.exceptions import WebDriverException -import unittest -import sys - -PORT = '8081' - -class TabbedQuestion_Tests(unittest.TestCase): - def setUp(self): - #self.driver = webdriver.Chrome() - #self.driver = webdriver.Firefox() # good for development - self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing - self.host = 'http://127.0.0.1:' + PORT - - +from runestone.unittest_base import module_fixture_maker, RunestoneTestCase + +setUpModule, tearDownModule = module_fixture_maker(__file__) + +class TabbedQuestion_Tests(RunestoneTestCase): def test_t1(self): '''Initial view. Tab 1 is visible, tab 2 is hidden''' self.driver.get(self.host + "/index.html") @@ -31,7 +20,7 @@ def test_t1(self): self.assertEqual("Tab 1", t1.text) self.assertEqual("Hello!", tp1.text) - + def test_t2(self): '''Tab 2 is visible, tab 1 is hidden''' self.driver.get(self.host + "/index.html") @@ -46,7 +35,7 @@ def test_t2(self): self.assertEqual("Tab 2", t1.text) self.assertEqual("Goodbye!", tp1.text) - + def test_t3(self): '''Tab 2 is selected, then tab 1''' self.driver.get(self.host + "/index.html") @@ -63,13 +52,3 @@ def test_t3(self): self.assertEqual("Tab 1", t1.text) self.assertEqual("Hello!", tp1.text) - - - def tearDown(self): - self.driver.quit() - - -if __name__ == '__main__': - if len(sys.argv) > 1: - PORT = sys.argv.pop() - unittest.main(verbosity=2) diff --git a/runestone/unittest_base.py b/runestone/unittest_base.py new file mode 100644 index 000000000..63a0b7692 --- /dev/null +++ b/runestone/unittest_base.py @@ -0,0 +1,63 @@ +import unittest +import os +import subprocess +from selenium import webdriver +from pyvirtualdisplay import Display + +# Select an unused port for serving web pages to the test suite. +PORT = '8081' + +# Define `module modules fixtures `_ to build the test Runestone project, run the server, then shut it down when the tests complete. +class ModuleFixture(object): + def __init__(self, + # The path to the Python module in which the test resides. This provides a simple way to determine the path in which to run runestone build/serve. + module_path): + + super(ModuleFixture, self).__init__() + self.base_path = os.path.dirname(module_path) + + def setUpModule(self): + # Change to this directory for running Runestone. + self.old_cwd = os.getcwd() + os.chdir(self.base_path) + # Compile the docs. + subprocess.check_call(['runestone', 'build', '--all']) + # Run the server. Simply calling ``runestone serve`` fails, since the process killed isn't the actual server, but probably a setuptools-created launcher. + self.runestone_server = subprocess.Popen(['python', '-m', 'runestone', 'serve', '--port', PORT]) + + def tearDownModule(self): + # Shut down the server. + self.runestone_server.kill() + # Restore the directory. + os.chdir(self.old_cwd) + +# Provide a simple way to instantiante a ModuleFixture in a test module. Typical use: +# +# .. code:: Python +# :number-lines: +# +# from unittest_base import module_fixture_maker +# setUpModule, tearDownModule = module_fixture_maker(__file__) +def module_fixture_maker(module_path): + mf = ModuleFixture(module_path) + return mf.setUpModule, mf.tearDownModule + +# Provide a base test case which sets up the `Selenium `_ driver. +class RunestoneTestCase(unittest.TestCase): + def setUp(self): + + self.display = Display(visible=0, size=(1280, 1024)) + self.display.start() + #self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing + # options = webdriver.ChromeOptions() + # options.add_argument("headless") + # options.add_argument("window-size=1200x800") + #self.driver = webdriver.Chrome(chrome_options=options) # good for development. + self.driver = webdriver.Chrome() # good for development. + + + self.host = 'http://127.0.0.1:' + PORT + + def tearDown(self): + self.driver.quit() + self.display.stop() diff --git a/runestone/usageAssignment/__init__.py b/runestone/usageAssignment/__init__.py index 7245ac2e3..a8086b808 100644 --- a/runestone/usageAssignment/__init__.py +++ b/runestone/usageAssignment/__init__.py @@ -23,7 +23,7 @@ from sqlalchemy import create_engine, Table, MetaData, select, delete from sqlalchemy.orm import sessionmaker from runestone.common.runestonedirective import RunestoneDirective -from runestone.server.componentdb import addAssignmentToDB, getOrCreateAssignmentType, getCourseID, addAssignmentQuestionToDB, getOrInsertQuestionForPage +from runestone.server.componentdb import addAssignmentToDB, getOrCreateAssignmentType, getCourseID, addAssignmentQuestionToDB, getOrInsertQuestionForPage, engine, meta from datetime import datetime from collections import OrderedDict import os @@ -65,7 +65,7 @@ def visit_ua_node(self,node): if d['ch'] not in chapters_and_subchapters: chapters_and_subchapters[d['ch']] = d['sub_chs'] else: - # The order matters with respect to the list wherein they're added to the dictionary. + # The order matters with respect to the list wherein they're added to the dictionary. for subch in d['sub_chs']: chapters_and_subchapters[d['ch']].append(subch) @@ -80,7 +80,7 @@ def visit_ua_node(self,node): s += '' s += '

' - # is this needed?? + # is this needed?? s = s.replace("u'","'") # hack: there must be a better way to include the list and avoid unicode strings self.body.append(s) @@ -141,14 +141,9 @@ def run(self): :points: """ - if all(name in os.environ for name in ['DBHOST', 'DBPASS', 'DBUSER', 'DBNAME']): - dburl = 'postgresql://{DBUSER}:{DBPASS}@{DBHOST}/{DBNAME}'.format(**os.environ) - else: - dburl = None + if not engine: self.state.document.settings.env.warn(self.state.document.settings.env.docname, "Environment variables not set for DB access; can't save usageassignment to DB") return [usageAssignmentNode(self.options)] - engine = create_engine(dburl) - meta = MetaData() # create a configured "Session" class Session = sessionmaker(bind=engine) session = Session() diff --git a/runestone/video/video.py b/runestone/video/video.py index 011df8371..4b5ded78a 100644 --- a/runestone/video/video.py +++ b/runestone/video/video.py @@ -18,7 +18,8 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive -from runestone.server.componentdb import addQuestionToDB +from runestone.server.componentdb import addQuestionToDB, addHTMLToDB +from runestone.common.runestonedirective import RunestoneDirective def setup(app): app.add_directive('video',Video) @@ -27,7 +28,7 @@ def setup(app): app.add_stylesheet('video.css') CODE = """\ -
+