diff --git a/.gitignore b/.gitignore index afdb3b116..afbe3115d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ nosetests.xml .idea bower_componenets/ test/ +.venv runestone/build_info diff --git a/requirements.txt b/requirements.txt index 0e32313c1..54b998662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,12 +8,12 @@ itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 Paver==1.2.4 -pkginfo==1.2.1 +pkginfo==1.3.2 Pygments==2.1.3 -pytz==2016.4 +pytz==2016.6.1 six==1.10.0 snowballstemmer==1.2.1 -Sphinx>=1.4.2 +Sphinx>=1.4.5 sphinx-rtd-theme==0.1.8 sphinxcontrib-paverutils==1.7 SQLAlchemy>=1.0.13 diff --git a/runestone/activecode/activecode.py b/runestone/activecode/activecode.py index 6f0e66dff..0536ee745 100644 --- a/runestone/activecode/activecode.py +++ b/runestone/activecode/activecode.py @@ -22,6 +22,7 @@ from docutils.parsers.rst import Directive from .textfield import * from sqlalchemy import create_engine, Table, MetaData, select, delete +from runestone.server.componentdb import addQuestionToDB try: from html import escape # py3 @@ -158,6 +159,9 @@ class ActiveCode(Directive): } def run(self): + + addQuestionToDB(self) + env = self.state.document.settings.env # keep track of how many activecodes we have.... could be used to automatically make a unique id for them. if not hasattr(env, 'activecodecounter'): diff --git a/runestone/assess/fitb.py b/runestone/assess/fitb.py index 370e5a356..dfc8d131b 100644 --- a/runestone/assess/fitb.py +++ b/runestone/assess/fitb.py @@ -21,6 +21,7 @@ from .assessbase import * import json import random +from runestone.server.componentdb import addQuestionToDB @@ -100,6 +101,7 @@ def run(self):

''' + addQuestionToDB(self) self.options['divid'] = self.arguments[0] diff --git a/runestone/assess/multiplechoice.py b/runestone/assess/multiplechoice.py index 609e38cc5..abafa5dc0 100644 --- a/runestone/assess/multiplechoice.py +++ b/runestone/assess/multiplechoice.py @@ -21,6 +21,7 @@ from .assessbase import * import json import random +from runestone.server.componentdb import addQuestionToDB class MChoiceNode(nodes.General, nodes.Element): @@ -154,7 +155,7 @@ def run(self): ''' - + addQuestionToDB(self) super(MChoice,self).run() diff --git a/runestone/clickableArea/clickable.py b/runestone/clickableArea/clickable.py index 10135c1e8..a3de018c2 100644 --- a/runestone/clickableArea/clickable.py +++ b/runestone/clickableArea/clickable.py @@ -19,6 +19,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive +from runestone.server.componentdb import addQuestionToDB def setup(app): app.add_directive('clickablearea',ClickableArea) @@ -115,6 +116,8 @@ def run(self): :incorrect: An array of the indices of the incorrect elements--same format as the correct elements. --Content-- """ + addQuestionToDB(self) + self.assert_has_content() self.options['divid'] = self.arguments[0] if "iscode" in self.options: diff --git a/runestone/codelens/visualizer.py b/runestone/codelens/visualizer.py index ee6053948..4ede4e8c4 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 def setup(app): app.add_directive('codelens', Codelens) @@ -180,6 +180,8 @@ class Codelens(Directive): def run(self): + addQuestionToDB(self) + self.JS_VARNAME = "" self.JS_VARVAL = "" diff --git a/runestone/dragndrop/dragndrop.py b/runestone/dragndrop/dragndrop.py index bbb2bce04..165e852d0 100644 --- a/runestone/dragndrop/dragndrop.py +++ b/runestone/dragndrop/dragndrop.py @@ -19,6 +19,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive +from runestone.server.componentdb import addQuestionToDB def setup(app): app.add_directive('dragndrop',DragNDrop) @@ -135,6 +136,8 @@ def run(self): The question goes here. """ + addQuestionToDB(self) + self.options['divid'] = self.arguments[0] if self.content: diff --git a/runestone/parsons/parsons.py b/runestone/parsons/parsons.py index c0f29117d..05bbe3e72 100644 --- a/runestone/parsons/parsons.py +++ b/runestone/parsons/parsons.py @@ -20,6 +20,7 @@ from docutils.parsers.rst import directives from docutils.parsers.rst import Directive from runestone.assess import Assessment +from runestone.server.componentdb import addQuestionToDB def setup(app): @@ -102,6 +103,8 @@ def findmax(alist): """ + addQuestionToDB(self) + TEMPLATE = '''
         %(qnumber)s: %(instructions)s%(code)s
diff --git a/runestone/poll/poll.py b/runestone/poll/poll.py index 4e9568168..25e43fb1d 100644 --- a/runestone/poll/poll.py +++ b/runestone/poll/poll.py @@ -19,6 +19,8 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive +from runestone.server.componentdb import addQuestionToDB + def setup(app): @@ -118,6 +120,7 @@ def run(self): :option_3: Option 3 ...etc...(Up to 10 options in mode 2) """ + addQuestionToDB(self) self.options['divid'] = self.arguments[0] if self.content: diff --git a/runestone/server/componentdb.py b/runestone/server/componentdb.py new file mode 100644 index 000000000..0ce87ce60 --- /dev/null +++ b/runestone/server/componentdb.py @@ -0,0 +1,88 @@ +# Copyright (C) 2016 Bradley N. Miller +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from __future__ import print_function + +from datetime import datetime + +__author__ = 'bmiller' + +import os +from sqlalchemy import create_engine, Table, MetaData, select, delete, update + + +def logSource(self): + sourcelog = self.state.document.settings.env.config.html_context.get('dsource', None) + if sourcelog: + with open(sourcelog, 'a') as sl: + sl.write("--------{}--------\n".format(self.arguments[0])) + sl.write(".. {}:: {}\n".format(self.name, " ".join(self.arguments))) + for k, v in self.options.items(): + sl.write(" :{}: {}\n".format(k, v)) + sl.write(" \n") + sl.write(" " + " \n".join(self.content)) + sl.write("\n") + + +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: + sl = "" + sl += ".. {}:: {}\n".format(self.name, " ".join(self.arguments)) + for k, v in self.options.items(): + sl += " :{}: {}\n".format(k, v) + sl += " \n" + sl += " " + " \n".join(self.content) + sl += "\n" + + basecourse = self.state.document.settings.env.config.html_context.get('basecourse', "unknown") + last_changed = datetime.now() + + engine = create_engine(dburl) + meta = MetaData() + questions = Table('questions', meta, autoload=True, autoload_with=engine) + if 'difficulty' in self.options: + difficulty = self.options['difficulty'] + else: + difficulty = 3 + + if 'author' in self.options: + author = self.options['author'] + else: + author = os.environ.get('USER','Brad Miller') + + srcpath, line = self.state_machine.get_source_and_line() + subchapter = os.path.basename(srcpath).replace('.rst','') + chapter = srcpath.split('/')[-2] + + sel = select([questions]).where(questions.c.name == self.arguments[0]) + res = engine.execute(sel).first() + if res: + if res['question'] != sl: + stmt = questions.update().where(questions.c.id == res['id']).values(question = sl, timestamp=last_changed) + engine.execute(stmt) + else: + ins = questions.insert().values(base_course=basecourse, name=self.arguments[0], + question=sl, timestamp=last_changed, is_private='F', + question_type=self.name, subchapter=subchapter, + author=author,difficulty=difficulty,chapter=chapter) + engine.execute(ins) + + + diff --git a/runestone/shortanswer/shortanswer.py b/runestone/shortanswer/shortanswer.py index d259d158e..bd9626edd 100755 --- a/runestone/shortanswer/shortanswer.py +++ b/runestone/shortanswer/shortanswer.py @@ -21,6 +21,7 @@ from docutils.parsers.rst import directives from docutils.parsers.rst import Directive from runestone.assess import Assessment +from runestone.server.componentdb import addQuestionToDB def setup(app): app.add_directive('shortanswer', JournalDirective) @@ -67,6 +68,8 @@ class JournalDirective(Assessment): def run(self): # Raise an error if the directive does not have contents. + addQuestionToDB(self) + self.assert_has_content() self.options['optional'] = 'data-optional' if 'optional' in self.options else '' diff --git a/runestone/video/video.py b/runestone/video/video.py index 8dd66c0e9..011df8371 100644 --- a/runestone/video/video.py +++ b/runestone/video/video.py @@ -18,7 +18,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst import Directive - +from runestone.server.componentdb import addQuestionToDB def setup(app): app.add_directive('video',Video) @@ -101,6 +101,8 @@ def run(self): :param self: :return: """ + addQuestionToDB(self) + mimeMap = {'mov':'mp4','webm':'webm', 'm4v':'m4v'} sources = [SOURCE % (directives.uri(line),mimeMap[line[line.rindex(".")+1:]]) for line in self.content]