Newer
Older

Jan Maximilian Michal
committed
#!/usr/bin/env python3
##########################################################################

Jan Maximilian Michal
committed
#
# This script contains the main part of hallgrim and is the only script that
# needs to be invoked. The steps it takes to generate a task are as follows:

Jan Maximilian Michal
committed
#
# * parse the commandline arguments with argparse
# * for each script determine the type and validate correct syntax
# * delegate the script to a handler for the specific type
# * the handler parses the script into the intermediate representation (mostly

Jan Maximilian Michal
committed
# arrays)
# * the handler passes the needed information to the script generator, which
# will print the final xml file.
# * a finisher compresses data if needed (needs to be implemented, maybe as
# separate subparser).

Jan Maximilian Michal
committed
#
##########################################################################

Jan Maximilian Michal
committed

Jan Maximilian Michal
committed
import importlib.util

Jan Maximilian Michal
committed
import argparse
import os
import configparser

Jan Maximilian Michal
committed
# local import

Jan Maximilian Michal
committed
from .IliasXMLCreator import packer
from .custom_markdown import get_markdown
from .messages import *
from .parser import choice_parser, gap_parser
from .uploader import send_script
from .templates import scaffolding
# set markdown
markdown = get_markdown()

Jan Maximilian Michal
committed

Jan Maximilian Michal
committed
def get_config():
config = configparser.ConfigParser()
config.read('config.ini')
if not config.sections():
error('Could not find config file.')
error('Please edit config.sample.ini and move it to config.ini')
error('Continue with default values. Script might fail.')
config['META'] = {'author': '__default__'}
return config

Jan Maximilian Michal
committed

Jan Maximilian Michal
committed
def file_to_module(name):

Jan Maximilian Michal
committed
return name.rstrip('.py').replace('/', '.')
def look_for_output():
if not os.path.exists('output'):
info('Created directory "output/"')
os.makedirs('output')
def type_selector(type):

Jan Maximilian Michal
committed
return 'MULTIPLE CHOICE QUESTION'
if 'single' in type:

Jan Maximilian Michal
committed
return 'SINGLE CHOICE QUESTION'

Jan Maximilian Michal
committed
if 'gap' in type:
return 'CLOZE QUESTION'

Jan Maximilian Michal
committed
def file_exists(path):
if not os.path.exists(path):
msg = 'The script "{}" does not exist.'.format(path)
raise argparse.ArgumentTypeError(msg)
return path

Jan Maximilian Michal
committed
def script_is_valid(script, required):
for field in required:
if not hasattr(script, field):
error("script does not export '{}' field.".format(field))
if any(not hasattr(script, field) for field in required):
abort("Script is invalid (see above)")

Jan Maximilian Michal
committed
def parseme():
config = get_config()

Jan Maximilian Michal
committed
parser = argparse.ArgumentParser()

Jan Maximilian Michal
committed
subparsers = parser.add_subparsers(dest="command")
parser_new = subparsers.add_parser(
"new", help="The utility the generate new scripts.")

Jan Maximilian Michal
committed
parser_new.add_argument(
"name",
help="The name of the new script",
metavar='NAME'

Jan Maximilian Michal
committed
)
parser_new.add_argument(
"-t",
"--type",
choices=['multi', 'single', 'gap', 'alignment'],
default='multi',
metavar='TYPE'
)
parser_new.add_argument(
"-a",
"--author",
help="Name of the scripts author",
default=config['META']['author'],

Jan Maximilian Michal
committed
metavar='AUTHOR'
)
parser_new.add_argument(
"-p",
"--points",
help='Points given for correct answer (different behavior for different questions)',
type=float,

Jan Maximilian Michal
committed
default=0.0,

Jan Maximilian Michal
committed
metavar='POINTS',
)
parser_gen = subparsers.add_parser(
"gen", help="Subcommand to convert from script to xml.")

Jan Maximilian Michal
committed
parser_gen.add_argument(

Jan Maximilian Michal
committed
'-o',
'--out',

Jan Maximilian Michal
committed
help='''Specify different output file. If no argument is given the Name
of the script is used.''',

Jan Maximilian Michal
committed
metavar='FILE')

Jan Maximilian Michal
committed
parser_gen.add_argument(

Jan Maximilian Michal
committed
help='Script to execute',

Jan Maximilian Michal
committed
nargs='+',

Jan Maximilian Michal
committed
type=file_exists,

Jan Maximilian Michal
committed
metavar='FILE')

Jan Maximilian Michal
committed
parser_gen.add_argument(
'-i',
'--instances',
help='How many instances should be produced (Only for parametrized questions).',
type=int,
default=1,
metavar='COUNT')

Jan Maximilian Michal
committed
parser_gen = subparsers.add_parser(
"upload", help="Subcommand to upload created xml instances.")
parser_gen.add_argument(
'script_list',
help='The scripts that should be uploaded',
nargs='+',
type=file_exists,
metavar='FILE')

Jan Maximilian Michal
committed
args = parser.parse_args()

Jan Maximilian Michal
committed
if args.command == 'gen':

Jan Maximilian Michal
committed
delegator(args.out, args.input, args.instances)
if args.command == 'upload':
handle_upload(args.script_list, config)

Jan Maximilian Michal
committed
if args.command == 'new':
handle_new_script(args.name, args.type, args.author, args.points)
if args.command == None:
parser.print_help()

Jan Maximilian Michal
committed
def delegator(output, script_list, instances):
"""
It gets a list of filenames and delegates them to the correct handler.
Every file that does not end with .py will be ignored. Each script
is imported and then passed as module to the handler.
Arguments:
output {filename} -- where to write the finished XML document
script_list {list} -- a list of filenames that contain scripts
instances {int} -- number of instances that should be generated
"""

Jan Maximilian Michal
committed
for script_name in filter(lambda a: a.endswith('.py'), script_list):

Jan Maximilian Michal
committed
module_name = os.path.basename(script_name)
spec = importlib.util.spec_from_file_location(module_name, script_name)

Jan Maximilian Michal
committed
script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(script)

Jan Maximilian Michal
committed
handler = {
'gap': handle_gap_questions,
'single': handle_choice_questions,

Jan Maximilian Michal
committed
'single choice': handle_choice_questions,
'multi': handle_choice_questions,

Jan Maximilian Michal
committed
'multiple choice': handle_choice_questions
}[script.meta['type']]

Jan Maximilian Michal
committed

Jan Maximilian Michal
committed
handler(output, script, instances)

Jan Maximilian Michal
committed

Jan Maximilian Michal
committed
def handle_gap_questions(output, script, instances):
""" Handles gap questions of all kinds
A script can contain any mixture of gap, numeric gap and choice gap
questions. The data object that is needed by the XML creating scripts
is generated and the task itself is handled by the parser. The parser
returns the intermediate representation of the task.
Arguments:
output {str} -- where to write the final file
script {module} -- the loaded module that describes the task
instances {int} -- number of instances that should be generated
"""

Jan Maximilian Michal
committed
script_is_valid(script, required=['meta', 'task', 'feedback'])
data = {
'type': type_selector(script.meta['type']),
'description': "_description",
'gap_list': gap_parser(script.task),
'author': script.meta['author'],
'title': script.meta['title'],
'shuffle': script.meta['shuffle'] if 'shuffle' in script.meta else True,
'feedback': markdown(script.feedback),
'gap_length': script.meta['gap_length'] if 'gap_length' in script.meta else 20,
}
output = os.path.join(
'output', script.meta['title']) + '.xml' if not output else output
packer.convert_and_print(data, output, instances)
info('Processed "{}" and'.format(script.__name__))
info('wrote xml "{}"'.format(output), notag=True)

Jan Maximilian Michal
committed
def handle_choice_questions(output, script, instances):
"""
Handles multiple and single choice questions. The relevant parts of the
script are fed into a parser that return the correct intermediate
representation for the task. In this case a list of answers.
Arguments:
output {str} -- where to write the finished XML document
script {module} -- the loaded module that describes the task
instances {int} -- number of instances that should be generated
"""

Jan Maximilian Michal
committed
script_is_valid(script, required=['meta', 'task', 'choices', 'feedback'])

Jan Maximilian Michal
committed
data = {

Jan Maximilian Michal
committed
'type': type_selector(script.meta['type']),

Jan Maximilian Michal
committed
'description': "_description",
'question_text': markdown(script.task),

Jan Maximilian Michal
committed
'author': script.meta['author'],
'title': script.meta['title'],
'maxattempts': '0',

Jan Maximilian Michal
committed
'shuffle': script.meta['shuffle'] if 'shuffle' in script.meta else True,
'questions': choice_parser(script.choices, script.meta['points']),

Jan Maximilian Michal
committed
'feedback': markdown(script.feedback)

Jan Maximilian Michal
committed
}
output = os.path.join(
'output', script.meta['title']) + '.xml' if not output else output

Jan Maximilian Michal
committed
packer.convert_and_print(data, output, instances)

Jan Maximilian Michal
committed
info('Processed "{}" and'.format(script.__name__))
info('wrote xml "{}"'.format(output), notag=True)
def handle_new_script(name, qtype, author, points):
""" Creates a new script file.
Takes in some meta information from the command line of if not present takes
it from the config.ini or uses default values.
Arguments:
name {str} -- name of the script, will also become filename
qtype {str} -- question type (choice, gap, alignment)
author {str} -- the author of the script
points {float} -- number of points for the task
"""
head, tail = os.path.split(name)
if not os.path.exists(head):
os.makedirs(head)
if not tail.endswith('.py'):
base = tail
else:
base = tail.rstrip('.py')
with open(os.path.join(head, base + '.py'), 'x') as new_script:

Jan Maximilian Michal
committed
if qtype in ['multiple choice', 'single choice']:
choice = '\nchoices = """\n[X] A\n[ ] B\n[ ] C\n[X] D\n"""\n'

Jan Maximilian Michal
committed
print(scaffolding.format(
author, base, qtype, points, choice).strip(), file=new_script)
info('Generated new script "{}."'.format(new_script.name))

Jan Maximilian Michal
committed
def handle_upload(script_list, config):
""" Passes data to the upload script.
The status code should be 500, since ILIAS always replies with that error
code after an upload is confirmed. If anything else the script will say
the status code was bad.
Arguments:
script_path {str} -- path to the file that should be uploaded
config {config object} -- the loaded configuration
"""
for script in script_list:
r = send_script(
script,
config['UPLAODER']['host'],
config['UPLAODER']['user'],
config['UPLAODER']['pass'],
config['UPLAODER']['rtoken'],
)
info("Uploaded %s. Status code looks %s." %
(script, "good" if r else "bad"))