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)]