diff --git a/runestone/__init__.py b/runestone/__init__.py
index eb0922d92..dbaa59fda 100644
--- a/runestone/__init__.py
+++ b/runestone/__init__.py
@@ -87,7 +87,7 @@ def build(options):
idxfile = os.path.join(options.build.sourcedir,'index.rst')
populateChapterInfo(options.build.project_name, idxfile)
- print('Creating Chapter Information')
+ print('Creating Chapter Information for {}'.format(idxfile))
except ImportError as e:
print('Chapter information database population skipped, This is OK for a standalone build.',e)
except Exception as e:
diff --git a/runestone/activecode/activecode.py b/runestone/activecode/activecode.py
index 393d1560b..dba2690ff 100644
--- a/runestone/activecode/activecode.py
+++ b/runestone/activecode/activecode.py
@@ -192,8 +192,8 @@ def run(self):
env.activecodecounter = 0
env.activecodecounter += 1
self.options['name'] = self.arguments[0].strip()
-
self.options['divid'] = self.arguments[0]
+
if not self.options['divid']:
raise Exception("No divid for ..activecode or ..actex in activecode.py")
diff --git a/runestone/activecode/js/activecode.js b/runestone/activecode/js/activecode.js
index af993fcec..91686ef61 100755
--- a/runestone/activecode/js/activecode.js
+++ b/runestone/activecode/js/activecode.js
@@ -459,11 +459,19 @@ ActiveCode.prototype.createGradeSummary = function () {
var report = eval(data)[0];
// check for report['message']
if (report) {
- var body = "
Grade Report
" +
- "This assignment: " + report['grade'] + "
" +
- "" + report['comment'] + "
" +
- "Number of graded assignments: " + report['count'] + "
" +
- "Average score: " + report['avg'] + "
"
+ if (report['version'] == 2){
+ // new version; would be better to embed this in HTML for the activecode
+ var body = "Grade Report
" +
+ "This question: " + report['grade'] + " out of " + report['max'] + "
" +
+ "" + report['comment'] + "
"
+ }
+ else{
+ var body = "Grade Report
" +
+ "This assignment: " + report['grade'] + "
" +
+ "" + report['comment'] + "
" +
+ "Number of graded assignments: " + report['count'] + "
" +
+ "Average score: " + report['avg'] + "
"
+ }
} else {
body = "The server did not return any grade information
";
diff --git a/runestone/assess/multiplechoice.py b/runestone/assess/multiplechoice.py
index 8a8b5bfe4..74297c2a1 100644
--- a/runestone/assess/multiplechoice.py
+++ b/runestone/assess/multiplechoice.py
@@ -21,7 +21,7 @@
from .assessbase import *
import json
import random
-from runestone.server.componentdb import addQuestionToDB
+from runestone.server.componentdb import addQuestionToDB, addHTMLToDB
class MChoiceNode(nodes.General, nodes.Element):
@@ -36,6 +36,10 @@ def __init__(self,content):
self.mc_options = content
def visit_mc_node(self,node):
+
+ node.delimiter = "_start__{}_".format(node.mc_options['divid'])
+ self.body.append(node.delimiter)
+
res = ""
if 'random' in node.mc_options:
node.mc_options['random'] = 'data-random'
@@ -70,9 +74,15 @@ def depart_mc_node(self,node):
res += node.template_end % node.mc_options
-
self.body.append(res)
+ addHTMLToDB(node.mc_options['divid'],
+ node.mc_options['basecourse'],
+ "".join(self.body[self.body.index(node.delimiter) + 1:]))
+
+ self.body.remove(node.delimiter)
+
+
diff --git a/runestone/assignment/__init__.py b/runestone/assignment/__init__.py
new file mode 100644
index 000000000..69376a1c0
--- /dev/null
+++ b/runestone/assignment/__init__.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2015 Paul Resnick
+#
+# 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
+
+__author__ = 'Paul Resnick'
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive
+from runestone.server.componentdb import addAssignmentToDB, addAssignmentQuestionToDB, getCourseID, getOrCreateAssignmentType, getQuestionID, get_HTML_from_DB
+from runestone.common.runestonedirective import RunestoneDirective
+from datetime import datetime
+
+def setup(app):
+ app.add_directive('assignment',Assignment)
+ app.add_node(AssignmentNode, html=(visit_a_node, depart_a_node))
+
+ app.connect('doctree-resolved',process_nodes)
+ app.connect('env-purge-doc', purge)
+
+class AssignmentNode(nodes.General, nodes.Element):
+ def __init__(self, content):
+ """
+
+ Arguments:
+ - `self`:
+ - `content`:
+ """
+ super(AssignmentNode, self).__init__(name=content['name'])
+ self.a_components = content
+
+
+def visit_a_node(self, node):
+ pass
+
+def depart_a_node(self, node):
+ question_ids = node.a_components['question_ids']
+ basecourse = node.a_components['basecourse']
+ for q_id in question_ids:
+ src = get_HTML_from_DB(q_id, basecourse)
+ if src:
+ self.body.append(src)
+ else:
+ self.body.append("Missing HTML source for {}
".format(q_id))
+ print("No HTML source saved for {}; can't include that kind of question until code writing to HTML is implemented for that directive".format(q_id))
+
+def process_nodes(app,env,docname):
+ pass
+
+
+def purge(app,env,docname):
+ pass
+
+
+
+class Assignment(RunestoneDirective):
+ """
+ .. assignment:
+ :name: Problem Set 1
+ :assignment_type: formative
+ :questions: (divid_1 50), (divid_2 100), ...
+ :deadline: 23-09-2016 15:30
+ :points: integer
+ """
+ required_arguments = 0 # not a sphinx_id for these; just a name parameter
+ optional_arguments = 0
+ has_content = False
+ option_spec = RunestoneDirective.option_spec.copy()
+ option_spec.update({
+ 'name': directives.unchanged,
+ 'assignment_type': directives.unchanged,
+ 'questions': directives.unchanged,
+ 'deadline':directives.unchanged,
+ 'points':directives.positive_int,
+ 'threshold': directives.positive_int,
+ 'autograde': directives.unchanged,
+ 'generate_html': directives.flag
+ })
+
+ def run(self):
+ """
+ .. assignment:
+ :name: Problem Set 1
+ :assignment_type: formative
+ :questions: (divid_1 50), (divid_2 100), ...
+ :deadline: 23-09-2016 15:30
+ :points: integer
+ :threshold: integer
+ :autograde: visited
+ :generate_html:
+ """
+
+ course_name = self.state.document.settings.env.config.html_context['course_id']
+ self.options['course_name'] = course_name
+ course_id = getCourseID(course_name)
+ basecourse_name = self.state.document.settings.env.config.html_context.get('basecourse', "unknown")
+ self.options['basecourse'] = self.state.document.settings.env.config.html_context.get('basecourse', "unknown")
+
+ name = self.options.get('name') # required; error if missing
+ assignment_type_name = self.options.get('assignment_type')
+ assignment_type_id = getOrCreateAssignmentType(assignment_type_name)
+
+ deadline = None
+
+ if 'deadline' in self.options:
+ try:
+ deadline = datetime.strptime(self.options['deadline'], '%Y-%m-%d %H:%M')
+ except:
+ try:
+ deadline = datetime.strptime(self.options['deadline'], '%Y-%m-%d %H:%M:%S')
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "deadline not in preferred format %Y-%m-%d %H:%M but accepting alternate format with seconds")
+ except:
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "deadline missing or incorrectly formatted; Omitting deadline")
+
+ points = self.options.get('points', 0)
+ threshold = self.options.get('threshold', None)
+ if threshold:
+ threshold = int(threshold)
+ autograde = self.options.get('autograde', None)
+
+ assignment_id = addAssignmentToDB(name = name,
+ course_id = course_id,
+ assignment_type_id = assignment_type_id,
+ deadline = deadline,
+ points = points,
+ threshold = threshold)
+
+ unparsed = self.options.get('questions', None)
+ question_names = []
+ if unparsed:
+ summative_type_id = getOrCreateAssignmentType("summative")
+ q_strings = unparsed.split(',')
+ for q in q_strings:
+ (question_name, points) = q.strip().split()
+ question_names.append(question_name)
+ # first get the question_id associated with question_name
+ question_id = getQuestionID(basecourse_name, question_name)
+ if question_id:
+ addAssignmentQuestionToDB(question_id, assignment_id, points, assessment_type = summative_type_id, autograde = autograde)
+ else:
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "Question {} is not in the database for basecourse {}".format(question_name, basecourse_name))
+ else:
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "No questions for assignment {}".format(name))
+ self.options['question_ids'] = question_names
+
+ if 'generate_html' in self.options:
+ return [AssignmentNode(self.options)]
+ else:
+ return []
diff --git a/runestone/common/runestonedirective.py b/runestone/common/runestonedirective.py
index 6317e0fac..87d260365 100644
--- a/runestone/common/runestonedirective.py
+++ b/runestone/common/runestonedirective.py
@@ -46,4 +46,3 @@ def __init__(self, *args, **kwargs):
self.options['basecourse'] = self.basecourse
self.options['chapter'] = self.chapter
self.options['subchapter'] = self.subchapter
-
diff --git a/runestone/external/__init__.py b/runestone/external/__init__.py
new file mode 100644
index 000000000..ff94a4bc0
--- /dev/null
+++ b/runestone/external/__init__.py
@@ -0,0 +1 @@
+from .external import *
diff --git a/runestone/external/css/external.css b/runestone/external/css/external.css
new file mode 100644
index 000000000..8768f5ec3
--- /dev/null
+++ b/runestone/external/css/external.css
@@ -0,0 +1,109 @@
+.modal-profile {
+ display:none;
+ min-height: 300px;
+ overflow: hidden;
+ width: 700px;
+ padding:25px;
+ border:1px solid #fff;
+ box-shadow: 0px 2px 7px #292929;
+ -moz-box-shadow: 0px 2px 7px #292929;
+ -webkit-box-shadow: 0px 2px 7px #292929;
+ border-radius:10px;
+ -moz-border-radius:10px;
+ -webkit-border-radius:10px;
+ background: #f2f2f2;
+ z-index:50;
+}
+
+.modal-lightsout {
+ display:none;
+ position:absolute;
+ top:0;
+ left:0;
+ width:100%;
+ z-index:25;
+ background:#000 ;
+}
+
+.modal-close-profile {
+ display:none;
+ position:absolute;
+ height: 43px;
+ width: 43px;
+ background-image: url('close.png');
+ top:1px;
+ right:0.5px;
+}
+
+
+.ac_actions{
+ text-align: center;
+}
+
+.ac_sep {
+ background: #000;
+ display: inline-block;
+ margin: -15px 10px;
+ width: 1px;
+ height: 35px;
+ padding: 0;
+ border: 0;
+}
+.ac_section{
+ position: relative;
+ clear:both;
+}
+.ac_section>* {
+ max-width: 500pt;
+ margin-left: auto;
+ margin-right: auto;
+ position:relative;
+}
+.ac_section .clearfix{
+ position: initial;
+}
+.ac_output{
+ display:none;
+ max-width: 450px;
+}
+
+.ac_caption {
+ text-align: center;
+ font-weight: bold;
+}
+
+.ac_caption_text {
+ font-weight: normal;
+}
+.ac_caption:before {
+ content: "ActiveCode: " counter(activecode) " ";
+ counter-increment: activecode;
+}
+
+.active_out {
+ background-color:#dcdcdc;
+ border-radius: 6px;
+ min-width: 20em;
+ max-height: 300px;
+ overflow: auto;
+}
+
+.visible-ac-canvas {
+ border: 2px solid black;
+}
+
+.ac_section>.col-md-12 {
+ max-width: 100% !important;
+}
+
+.full_width ol {
+ max-width: 100% !important;
+}
+
+.ac-disabled {
+ pointer-events: none;
+}
+
+.ac-feedback {
+ border: 1px solid black;
+}
diff --git a/runestone/external/external.py b/runestone/external/external.py
new file mode 100644
index 000000000..8ba2ab76d
--- /dev/null
+++ b/runestone/external/external.py
@@ -0,0 +1,112 @@
+# Copyright (C) 2011 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 docutils import nodes
+from docutils.parsers.rst import directives
+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
+
+try:
+ from html import escape # py3
+except ImportError:
+ from cgi import escape # py2
+
+__author__ = 'jczetta'
+# 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.
+
+
+def setup(app):
+ app.add_directive('external', ExternalDirective)
+
+ app.add_node(ExternalNode, html=(visit_external_node, depart_external_node))
+
+
+class ExternalNode(nodes.General, nodes.Element):
+ def __init__(self, content):
+ super(ExternalNode, self).__init__()
+ self.external_options = content
+
+
+def visit_external_node(self, node):
+ # Set options and format templates accordingly
+ # env = node.document.settings.env
+
+ node.delimiter = "_start__{}_".format(node.external_options['divid'])
+
+ self.body.append(node.delimiter)
+
+ res = TEMPLATE_START % node.external_options
+ self.body.append(res)
+
+
+def depart_external_node(self, node):
+ # Set options and format templates accordingly
+ res = TEMPLATE_END % node.external_options
+
+ self.body.append(res)
+
+ addHTMLToDB(node.external_options['divid'],
+ node.external_options['basecourse'],
+ "".join(""))
+
+ self.body.remove(node.delimiter)
+
+# Templates to be formatted by node options
+TEMPLATE_START = '''
+
+
+
+ '''
+TEMPLATE_END = '''
+
+
+ '''
+
+
+class ExternalDirective(RunestoneDirective):
+ """
+.. external:: identifier
+
+ Content Everything here is part of the activity
+ Content Can include links...
+ """
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+ option_spec = RunestoneDirective.option_spec.copy()
+ option_spec.update({'number': directives.positive_int})
+
+ def run(self):
+ addQuestionToDB(self)
+
+ self.assert_has_content() # make sure activity has something in it
+ self.options['divid'] = self.arguments[0]
+ self.options['basecourse'] = self.state.document.settings.env.config.html_context.get('basecourse', "unknown")
+
+ self.options['name'] = self.arguments[0].strip()
+
+ external_node = ExternalNode(self.options)
+ self.add_name(external_node)
+
+ self.state.nested_parse(self.content, self.content_offset, external_node)
+
+ return [external_node]
diff --git a/runestone/server/chapternames.py b/runestone/server/chapternames.py
index 426ce0549..c4445e5be 100644
--- a/runestone/server/chapternames.py
+++ b/runestone/server/chapternames.py
@@ -38,8 +38,8 @@ def findChaptersSubChapters(tocfile):
else:
stop = toclines[i + 1]
for j in range(start, stop):
- if ".rst" in ftext[j] and os.path.sep in ftext[j]:
- chapter, subchapter = ftext[j].strip()[:-4].split(os.path.sep)
+ if ".rst" in ftext[j] and "/" in ftext[j]:
+ chapter, subchapter = ftext[j].strip()[:-4].split('/')
chapter = chapter.strip()
subchapter = subchapter.strip()
if chapter not in chdict:
@@ -62,10 +62,10 @@ def findV2ChaptersSubChapters(tocfile):
toclines = getTOCEntries(ftext)
chdict = OrderedDict()
chtitles = {}
- toclines = [x for x in toclines if os.path.sep in x]
+ toclines = [x for x in toclines if '/' in x]
basepath = os.path.dirname(tocfile)
for subchapter in toclines:
- chapter = subchapter.split(os.path.sep)[0]
+ chapter = subchapter.split('/')[0]
with open(os.path.join(basepath,subchapter),'r') as scfile:
ft = scfile.readline().strip()
chtitles[chapter] = ft
diff --git a/runestone/server/componentdb.py b/runestone/server/componentdb.py
index 686262a38..84b9ce26b 100644
--- a/runestone/server/componentdb.py
+++ b/runestone/server/componentdb.py
@@ -22,6 +22,14 @@
import os
from sqlalchemy import create_engine, Table, MetaData, select, delete, update, and_
+# 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)
+ engine = create_engine(dburl)
+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)
@@ -66,8 +74,10 @@ def addQuestionToDB(self):
srcpath, line = self.state_machine.get_source_and_line()
subchapter = os.path.basename(srcpath).replace('.rst','')
chapter = srcpath.split(os.path.sep)[-2]
+
autograde = self.options.get('autograde', None)
+
sel = select([questions]).where(and_(questions.c.name == self.arguments[0],
questions.c.base_course == basecourse))
res = engine.execute(sel).first()
@@ -82,6 +92,155 @@ def addQuestionToDB(self):
except UnicodeEncodeError:
raise self.severe("Bad character in directive {} in {}/{} this will not be saved to the DB".format(self.arguments[0], self.chapter, self.subchapter))
+def getQuestionID(base_course, name):
+ meta = MetaData()
+ questions = Table('questions', meta, autoload=True, autoload_with=engine)
+
+
+ sel = select([questions]).where(and_(questions.c.name == name,
+ questions.c.base_course == base_course))
+ res = engine.execute(sel).first()
+ if res:
+ return res['id']
+ else:
+ return None
+
+def getOrInsertQuestionForPage(base_course=None, name=None, is_private='F', question_type="page", autograde = "visited", author=None, difficulty=1,chapter=None):
+ last_changed = datetime.now()
+
+ meta = MetaData()
+ questions = Table('questions', meta, autoload=True, autoload_with=engine)
+
+
+ sel = select([questions]).where(and_(questions.c.name == name,
+ questions.c.base_course == base_course))
+ res = engine.execute(sel).first()
+
+ if res:
+ id = res['id']
+ stmt = questions.update().where(questions.c.id == id).values(
+ timestamp=last_changed,
+ is_private= is_private,
+ question_type=question_type,
+ autograde = autograde,
+ author=author,
+ difficulty=difficulty,
+ chapter=chapter)
+ res = engine.execute(stmt)
+ return id
+ else:
+ ins = questions.insert().values(
+ base_course= base_course,
+ name= name,
+ timestamp=last_changed,
+ is_private= is_private,
+ question_type=question_type,
+ autograde = autograde,
+ author=author,
+ difficulty=difficulty,
+ chapter=chapter)
+ res = engine.execute(ins)
+ return res.inserted_primary_key[0]
+
+def getOrCreateAssignmentType(assignment_type_name, grade_type = None, points_possible = None, assignments_count = None, assignments_dropped = None):
+
+ meta = MetaData()
+ assignment_types = Table('assignment_types', meta, autoload=True, autoload_with=engine)
+
+ # search for it in the DB
+ sel = select([assignment_types]).where(assignment_types.c.name == assignment_type_name)
+ res = engine.execute(sel).first()
+ if res:
+ return res['id']
+ else:
+ # create the assignment type
+ ins = assignment_types.insert().values(
+ name=assignment_type_name,
+ grade_type = grade_type,
+ points_possible = points_possible,
+ assignments_count = assignments_count,
+ assignments_dropped = assignments_dropped)
+ res = engine.execute(ins)
+ return res.inserted_primary_key[0]
+
+def addAssignmentQuestionToDB(question_id, assignment_id, points, assessment_type = None, timed=None, autograde=None):
+ meta = MetaData()
+ questions = Table('questions', meta, autoload=True, autoload_with=engine)
+ assignment_questions = Table('assignment_questions', meta, autoload=True, autoload_with=engine)
+
+ # now insert or update the assignment_questions row
+ sel = select([assignment_questions]).where(and_(assignment_questions.c.assignment_id == assignment_id,
+ assignment_questions.c.question_id == question_id))
+ res = engine.execute(sel).first()
+ if res:
+ #update
+ stmt = assignment_questions.update().where(assignment_questions.c.id == res['id']).values( \
+ assignment_id = assignment_id,
+ question_id = question_id,
+ points = points,
+ timed= timed,
+ assessment_type = assessment_type,
+ autograde = autograde
+ )
+ engine.execute(stmt)
+ else:
+ #insert
+ ins = assignment_questions.insert().values(
+ assignment_id = assignment_id,
+ question_id = question_id,
+ points = points,
+ timed=timed,
+ assessment_type = assessment_type,
+ autograde = autograde
+ )
+ engine.execute(ins)
+
+def getCourseID(coursename):
+ meta = MetaData()
+ courses = Table('courses', meta, autoload=True, autoload_with=engine)
+
+ sel = select([courses]).where(courses.c.course_name == coursename)
+ res = engine.execute(sel).first()
+ return res['id']
+
+def addAssignmentToDB(name = None, course_id = None, assignment_type_id = None, deadline = None, points = None, threshold = None):
+
+ last_changed = datetime.now()
+
+ meta = MetaData()
+ assignments = Table('assignments', meta, autoload=True, autoload_with=engine)
+ assignment_questions = Table('assignment_questions', meta, autoload=True, autoload_with=engine)
+
+ sel = select([assignments]).where(and_(assignments.c.name == name,
+ assignments.c.course == course_id))
+ res = engine.execute(sel).first()
+ if res:
+ stmt = assignments.update().where(assignments.c.id == res['id']).values(
+ assignment_type = assignment_type_id,
+ duedate = deadline,
+ points = points,
+ threshold = threshold
+ )
+ engine.execute(stmt)
+ a_id = res['id']
+ # delete all existing AssignmentQuestions, so that you don't have any leftovers
+ # this is safe because grades and comments are associated with div_ids and course_names, not assignment_questions rows.
+ stmt2 = assignment_questions.delete().where(assignment_questions.c.assignment_id == a_id)
+ engine.execute(stmt2)
+
+ else:
+ ins = assignments.insert().values(
+ course=course_id,
+ name=name,
+ assignment_type = assignment_type_id,
+ duedate = deadline,
+ points = points,
+ threshold = threshold)
+ res = engine.execute(ins)
+ a_id = res.inserted_primary_key[0]
+
+ 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)
@@ -106,3 +265,13 @@ def addHTMLToDB(divid, basecourse, htmlsrc):
except:
print("Error while trying to add directive {} to the DB".format(divid))
+def get_HTML_from_DB(divid, basecourse):
+ meta = MetaData()
+ questions = Table('questions', meta, autoload=True, autoload_with=engine)
+ sel = select([questions]).where(and_(questions.c.name == divid,
+ questions.c.base_course == basecourse))
+ res = engine.execute(sel).first()
+ if res:
+ return res['htmlsrc']
+ else:
+ return ""
diff --git a/runestone/usageAssignment/__init__.py b/runestone/usageAssignment/__init__.py
index b4216f1f1..7245ac2e3 100644
--- a/runestone/usageAssignment/__init__.py
+++ b/runestone/usageAssignment/__init__.py
@@ -23,6 +23,10 @@
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 datetime import datetime
+from collections import OrderedDict
+import os
def setup(app):
app.add_directive('usageassignment',usageAssignment)
@@ -54,10 +58,19 @@ def visit_ua_node(self,node):
chapter_data = None
s = ""
+ chapters_and_subchapters = OrderedDict()
if chapter_data and course_name:
- for d in chapter_data:
+ for d in chapter_data: # Set up Chapter-Subchs dictionary
ch_name, sub_chs = d['ch'], d['sub_chs']
- s += ''
+ 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.
+ for subch in d['sub_chs']:
+ chapters_and_subchapters[d['ch']].append(subch)
+
+ for ch_name,sub_chs in chapters_and_subchapters.items():
+ s += '
'
s += ch_name
s += '
'
for sub_ch_name in sub_chs:
@@ -67,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)
@@ -100,7 +113,7 @@ class usageAssignment(Directive):
:pct_required:
:points:
"""
- required_arguments = 1 # requires an id for the directive
+ required_arguments = 0 # use assignment_name parameter
optional_arguments = 0
has_content = False
option_spec = RunestoneDirective.option_spec.copy()
@@ -115,32 +128,6 @@ class usageAssignment(Directive):
'points':directives.positive_int
})
- def get_or_make_assignment_type(self, engine, session, AssignmentType):
- # There will normally be only one "usage" type of assignment; we'll take that, or create it if it doesn't exist
- a = session.query(AssignmentType).filter(AssignmentType.c.grade_type == 'use').first()
- if not a:
- print("creating Lecture Prep assignment type")
- engine.execute(AssignmentType.insert().values(
- name = 'Lecture Prep',
- grade_type = 'use',
- points_possible = '50',
- assignments_count = 23,
- assignments_dropped = 3))
- a = session.query(AssignmentType).filter(AssignmentType.c.grade_type == 'use').first()
- return a.id
-
- def get_or_make_course_section(self, course_id, engine, session, Section):
- # Currently only works with a single default section for the whole course
- # Could add sections to the directive to allow different sections to get different deadlines
- a = session.query(Section).filter(Section.c.course_id == course_id).first()
- if not a:
- print("creating default course section")
- engine.execute(Section.insert().values(
- course_id = course_id,
- name = 'Default Section'))
- a = session.query(Section).filter(Section.c.course_id == course_id).first()
- return a.id
-
def run(self):
"""
.. usageassignment:: prep_1
@@ -153,39 +140,37 @@ def run(self):
:pct_required:
:points:
"""
- self.options['divid'] = self.arguments[0]
- try:
- env = self.state.document.settings.env
- # print("DBURL = ",env.config['dburl'])
- engine = create_engine(env.config.html_context['dburl'])
- meta = MetaData()
- Assignment = Table('assignments', meta, autoload=True, autoload_with=engine)
- Chapter = Table('chapters', meta, autoload=True, autoload_with=engine)
- SubChapter = Table('sub_chapters', meta, autoload=True, autoload_with=engine)
- Problem = Table('problems', meta, autoload=True, autoload_with=engine)
- Div = Table('div_ids', meta, autoload=True, autoload_with=engine)
- Course = Table('courses', meta, autoload=True, autoload_with=engine)
- PIPDeadline = Table('pipactex_deadline', meta, autoload=True, autoload_with=engine)
- Deadline = Table('deadlines', meta, autoload=True, autoload_with=engine)
- AssignmentType = Table('assignment_types', meta, autoload=True, autoload_with=engine)
- Section = Table('sections', meta, autoload=True, autoload_with=engine)
- # create a configured "Session" class
- Session = sessionmaker(bind=engine)
- except:
- print("Unable to create and save usage assignment. 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")
- print()
- print("This should only affect the grading interface. Everything else should be fine.")
+
+ 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
+ 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()
+ Chapter = Table('chapters', meta, autoload=True, autoload_with=engine)
+ SubChapter = Table('sub_chapters', meta, autoload=True, autoload_with=engine)
+ Problem = Table('problems', meta, autoload=True, autoload_with=engine)
+ Div = Table('div_ids', meta, autoload=True, autoload_with=engine)
+ AssignmentType = Table('assignment_types', meta, autoload=True, autoload_with=engine)
+ Section = Table('sections', meta, autoload=True, autoload_with=engine)
+
+ assignment_type_id = getOrCreateAssignmentType("Lecture Prep",
+ grade_type = 'use',
+ points_possible = '50',
+ assignments_count = 23,
+ assignments_dropped = 3)
- # create a Session
- session = Session()
- course_name = env.config.html_context['course_id']
+ course_name = self.state.document.settings.env.config.html_context['course_id']
self.options['course_name'] = course_name
- course_id = str(session.query(Course).filter(Course.c.course_name == course_name).first().id)
+ course_id = getCourseID(course_name)
+ basecourse_name = self.state.document.settings.env.config.html_context.get('basecourse', "unknown")
# Accumulate all the Chapters and SubChapters that are to be visited
# For each chapter, accumulate all subchapters
@@ -202,8 +187,9 @@ def run(self):
sub_chs += results
chapter_data = {'ch': nm, 'sub_chs': [r.sub_chapter_label for r in results]}
self.options['chapter_data'].append(chapter_data)
+
except:
- print("Chapters requested not found: %s" % (self.options.get('chapters')))
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "Chapters requested not found: %s" % (self.options.get('chapters')))
# Add any explicit subchapters
if 'subchapters' in self.options:
try:
@@ -213,10 +199,10 @@ def run(self):
subch = session.query(SubChapter).filter(SubChapter.c.chapter_id == ch_id, SubChapter.c.sub_chapter_label == subch_name).first()
sub_chs.append(subch)
if not subch:
- print("problem with: %s" % nm)
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "problem with: %s" % nm)
self.options['chapter_data'].append({'ch': ch_dir, 'sub_chs': [subch_name]})
except:
- print("Subchapters requested not found: %s" % (self.options.get('subchapters')))
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "Subchapters requested not found: %s" % (self.options.get('subchapters')))
# Accumulate all the ActiveCodes that are to be run and URL paths to be visited
divs = []
@@ -229,66 +215,36 @@ def run(self):
Div.c.subchapter == subch.sub_chapter_label).all()
paths.append('/runestone/static/%s/%s/%s.html' % (course_name, ch_name, subch.sub_chapter_label))
except:
- print ("Subchapter not found: %s" % (subch))
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "Subchapter not found: %s" % (subch))
tracked_div_types = ['activecode', 'actex']
active_codes = [d.div_id for d in divs if d.div_type in tracked_div_types]
min_activities = (len(paths) + len(active_codes)) * self.options.get('pct_required', 0) / 100
- assignment_type = self.get_or_make_assignment_type(engine, session, AssignmentType)
-
- # Add or update Assignment
- a = session.query(Assignment).filter(Assignment.c.name == self.options.get('assignment_name', 'dummy_assignment'), Assignment.c.course == course_id).first()
- if a:
- engine.execute(Assignment.update()\
- .where(Assignment.c.name == self.options.get('assignment_name', 'dummy_assignment'))\
- .where(Assignment.c.course == course_id)\
- .values(
- name = self.options.get('assignment_name', 'dummy_assignment'),
- assignment_type = assignment_type,
- points = self.options.get('points', 0),
- threshold = min_activities,
- ))
- else:
- engine.execute(Assignment.insert().values(
- course = course_id,
- assignment_type = assignment_type,
- name = self.options.get('assignment_name', 'dummy_assignment'),
- points = self.options.get('points', 0),
- threshold = min_activities
- ))
- a = session.query(Assignment).filter(Assignment.c.name == self.options.get('assignment_name', 'dummy_assignment'), Assignment.c.course == course_id).first()
-
- # Replace any existing deadlines
- section_id = self.get_or_make_course_section(course_id, engine, session, Section)
- engine.execute(Deadline.delete()\
- .where(Deadline.c.section == section_id)\
- .where(Deadline.c.assignment == a.id))
+ deadline = None
if 'deadline' in self.options:
- engine.execute(Deadline.insert()\
- .values(section = section_id,
- assignment = a.id,
- deadline = self.options.get('deadline')))
- #I think pipactex_deadlines are deprected; let's see if anything breaks by just deleting them
- for acid in active_codes:
- engine.execute(PIPDeadline.delete()\
- .where(PIPDeadline.c.section == section_id)\
- .where(PIPDeadline.c.acid_prefix == acid))
- # if 'deadline' in self.options:
- # engine.execute(Deadline.insert()\
- # .values(section = section_id,
- # acid_prefix = acid,
- # deadline = self.options.get('deadline')))
- #
- # replace any existing problems for this assignment with the new ones
- engine.execute(Problem.delete()\
- .where(Problem.c.assignment == a.id))
- for acid in paths + active_codes:
- engine.execute(Problem.insert()\
- .values(acid = acid, assignment = a.id))
+ try:
+ deadline = datetime.strptime(self.options['deadline'], '%Y-%m-%d %H:%M')
+ except:
+ try:
+ deadline = datetime.strptime(self.options['deadline'], '%Y-%m-%d %H:%M:%S')
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "deadline not in preferred format %Y-%m-%d %H:%M but accepting alternate format with seconds")
+ except:
+ self.state.document.settings.env.warn(self.state.document.settings.env.docname, "deadline missing or incorrectly formatted; Omitting deadline")
- session.commit()
+ points = self.options.get('points', 0)
+
+ assignment_id = addAssignmentToDB(name = self.options.get('assignment_name', 'dummy_assignment'),
+ course_id = course_id,
+ assignment_type_id = assignment_type_id,
+ deadline = deadline,
+ points = points,
+ threshold = min_activities)
+
+ for acid in paths + active_codes:
+ q_id = getOrInsertQuestionForPage(base_course=basecourse_name, name=acid, is_private='F', question_type="page", autograde = "visited", difficulty=1,chapter=None)
+ addAssignmentQuestionToDB(q_id, assignment_id, 1, autograde="visited")
return [usageAssignmentNode(self.options)]