diff options
author | Karl Berry <karl@freefriends.org> | 2019-03-16 22:43:33 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2019-03-16 22:43:33 +0000 |
commit | 7e80fa208736f4ab99b1d57dd1571da41f1e3744 (patch) | |
tree | 606bd82a5737d587ef239fd58a5a58c256666178 /Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py | |
parent | 9de7d0ea371387f647487812b48b51030c739bdf (diff) |
webquiz
git-svn-id: svn://tug.org/texlive/trunk@50419 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py')
-rwxr-xr-x | Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py b/Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py new file mode 100755 index 00000000000..cca12a664f5 --- /dev/null +++ b/Master/texmf-dist/scripts/webquiz/webquiz_makequiz.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python3 +r''' +------------------------------------------------------------------------------ + webquiz_makequiz | build and generate the quiz page using webquiz_xml + | and webquiz_layout +------------------------------------------------------------------------------ + Copyright (C) Andrew Mathas, University of Sydney + + Distributed under the terms of the GNU General Public License (GPL) + http://www.gnu.org/licenses/ + + This file is part of the WebQuiz system. + + <Andrew.Mathas@sydney.edu.au> + <Donald.Taylor@sydney.edu.au> +------------------------------------------------------------------------------ +''' +import subprocess +import codecs +import shutil +import os +import re + +import webquiz_templates +import webquiz_util +import webquiz_xml + +################################################################################# +class MakeWebQuiz(object): + """ + Convert a webquiz latex file to an online quiz. + + There are several steps: + 1. If given a LaTeX file then run htlatex/make4ht on the latex file to generate an + xml file for the quiz that has all LaTeS markup converted to html. + 2. Read in the xml file version of the quiz + 3. Spit out the html version + + The HTMl is constructed using the template strings in webquiz_templates + """ + # attributes that will form part of the generated web page + header = '' # page header: title, meta data, links + css = '' # css specifications + javascript = '' # javascript code + quiz_questions = '' # the main quiz page + side_menu = '' # the left hand quiz menu + + def __init__(self, quiz_name, quiz_file, options, settings, metadata): + self.options = options + self.settings = settings + self.metadata = metadata + self.quiz_name = quiz_name.split('.')[0] + self.quiz_file, extension = quiz_file.split('.') + self.webquiz_url = settings['webquiz_url'] + if self.webquiz_url[-1] == '/': + self.webquiz_url = self.webquiz_url[:len(self.webquiz_url)-1] + + # run htlatex only if quiz_file has a .tex extension + if extension == 'tex': + self.htlatex_quiz_file() + + self.read_xml_file() + + # use kpsewhich to fine the webquiz language file + try: + language_file = webquiz_util.kpsewhich('webquiz-{}.lang'.format(self.quiz.language)) + except subprocess.CalledProcessError: + self.webquiz_error( + 'kpsewhich is unable to find language file for "{}"'.format(self.quiz.language) + ) + # read the language file and store as a dictonary + self.language = webquiz_util.MetaData(language_file) + + # initialise number of quiz and discussion items + self.number_discussions = len(self.quiz.discussion_list) + self.number_questions = len(self.quiz.question_list) + + # build the different components of the quiz web page + self.add_meta_data() + self.add_question_javascript() + self.add_side_menu() + self.add_quiz_header_and_questions() + self.add_breadcrumbs() + + # add the initialisation warning if webquiz has not been initialised + if self.settings.initialise_warning != '': + self.breadcrumbs = self.settings.initialise_warning + self.breadcrumbs + + # now write the quiz to the html file + with codecs.open(self.quiz_name + '.html', 'w', encoding='utf8') as file: + # write the quiz in the specified format + file.write(self.options.write_web_page(self)) + + def webquiz_debug(self, msg): + r''' + Customised debugging message for the makequiz module + ''' + webquiz_util.webquiz_debug(self.settings.debugging, 'makequiz: '+msg) + + def webquiz_error(self, msg, err=None): + r''' + Customised eror message for the makequiz module + ''' + webquiz_util.webquiz_error(self.settings.debugging, 'makequiz: '+msg, err) + + def add_breadcrumbs(self): + r''' + Build the breadcrumbs by appropriately parsing the magic crumbs + ''' + self.breadcrumbs = '' + if self.quiz.breadcrumbs != '': + # build the bread crumbs + crumbs = '\n ' + for crumb in self.quiz.breadcrumbs.split('|'): + crumb = crumb.strip() + + if crumb == 'breadcrumb': + crumbs += self.add_breadcrumb_line(self.quiz.breadcrumb, missing='breadcrumb') + + elif crumb == 'department': + crumbs += self.add_breadcrumb_line( + text=self.quiz.department, + url=self.quiz.department_url, + missing='department') + + elif crumb == 'institution': + crumbs += self.add_breadcrumb_line( + text=self.quiz.institution, + url=self.quiz.institution_url, + missing='institution') + + elif crumb == 'quizindex': + if self.quiz.quiz_index == []: + crumbs += webquiz_templates.breadcrumb_quizindex.format( + quizzes_url=self.quiz.quizzes_url, + **self.language) + else: + crumbs += self.add_breadcrumb_line('Quizzes') + + elif crumb == 'Title': + crumbs += self.add_breadcrumb_line(text = self.quiz.title, missing='title') + + elif crumb == 'title': + title = self.quiz.title + crumbs += self.add_breadcrumb_line( + text = title[:title.index(':')] if ':' in title else title, + missing='title' + ) + + elif crumb == 'unitcode': + crumbs += self.add_breadcrumb_line( + text=self.quiz.unit_code, + url=self.quiz.unit_url, + missing='unit code') + + elif crumb == 'unitname': + crumbs += self.add_breadcrumb_line( + text=self.quiz.unit_name, + url=self.quiz.unit_url, + missing='unit name') + + elif crumb != '': + lastSpace = crumb.rfind(' ') + url = crumb[lastSpace:].strip() + if url[0] == '/' or url.lower()[:4] == 'http': + crumbs += self.add_breadcrumb_line(text=crumb[:lastSpace], url=url) + else: + crumbs += self.add_breadcrumb_line(crumb) + + self.breadcrumbs = webquiz_templates.breadcrumbs.format(crumbs=crumbs) + + def add_breadcrumb_line(self, text, url='', missing='??'): + r''' + Return a line to add the bread crumbs, with errors if necessary + ''' + if url == '': + return webquiz_templates.breadcrumb_line_text.format( + text=text if text != '' else '?? ' + missing) + + return webquiz_templates.breadcrumb_line_url.format( + url=url, text=text if text != '' else '?? ' + missing) + + def htlatex_quiz_file(self): + r''' + Process the file using htlatex/make4ht. This converts the quiz to an xml + with markup specifying the different elements of the quiz page. + ''' + # at the minimum we put a css file into a <quiz_name> subdirectory + if not os.path.exists(self.quiz_name): + os.makedirs(self.quiz_name) + + try: + self.options.talk('Processing {}.tex with TeX4ht'.format(self.quiz_name)) + # there is a slightly torturous process to convert the engine + # settings into a command line option that make4ht understands + cmd = 'make4ht --utf8 --config webquiz.cfg {draft} {engine} {escape} {make4ht_options} {quiz_file}.tex'.format( + draft='--mode draft' if self.options.draft else '', + engine=self.settings.settings['engine']['values'][self.options.engine], + escape='--shell-escape' if self.options.shell_escape else '', + make4ht_options=self.options.make4ht_options, + quiz_file=self.quiz_file + ) + self.options.run(cmd) + + # move the css file into the quiz_file subdirectory + if os.path.exists(self.quiz_file + '.css'): + shutil.move( + self.quiz_file + '.css', + os.path.join(self.quiz_name, self.quiz_name + '.css')) + + # Now move any images that were created into the quiz_name + # subdirectory and update the links in the html file As htlatex + # generates an html file, we rename this as an xml file at the same + # time - in the cfg file, \Preamable{ext=xml} should lead to an xml + # file being created but this doesn't seem to work ?? + try: + fix_img = re.compile(r'^(|.* )\b(data|src)="([-0-9a-zA-Z]*\.(?:png|svg))" (.*)$') + with codecs.open(self.quiz_file + '.html', 'r', encoding='utf8') as make4ht_file: + with codecs.open(self.quiz_name + '.xml', 'w', encoding='utf8') as xml_file: + for line in make4ht_file: + match = fix_img.match(line) + if match is None: + xml_file.write(line) + else: + # update html link and move file + start, src, image, rest_of_line = match.groups() + xml_file.write(r'{}{}="{}/{}" {}'.format( + start, src, self.quiz_name, image, rest_of_line)) + shutil.move(image, os.path.join(self.quiz_name, image)) + + except OSError as err: + self.webquiz_error( + 'there was a problem moving the image files for {}'.format( + self.quiz_name), err) + + except Exception as err: + self.webquiz_error( 'something went wrong when running htlatex on {}'.format(self.quiz_file), err) + + def read_xml_file(self): + r''' + Read in the webquiz xml file for the quiz and store the xml document + tree in ``self.quiz``. + ''' + try: + # read in the xml version of the quiz + if not os.path.isfile(self.quiz_name + '.xml'): + self.webquiz_error('{}.xml does not exist!?'.format(self.quiz_name)) + self.quiz = webquiz_xml.ReadWebQuizXmlFile(self.quiz_name + '.xml', self.settings) + except Exception as err: + self.webquiz_error('error reading the xml generated for {}. Please check your latex source.' + .format(self.quiz_name), err) + + def add_meta_data(self): + """ add the meta data for the web page to self.header """ + # meta tags` + self.header += webquiz_templates.html_meta.format( + version=self.metadata.version, + authors=self.metadata.authors, + webquiz_url=self.webquiz_url, + description=self.metadata.description, + copyright=self.metadata.copyright, + department=self.quiz.department, + institution=self.quiz.institution, + quiz_file=self.quiz_name, + theme=self.quiz.theme) + if self.quiz.mathjs: + self.header += webquiz_templates.mathjs + + def add_side_menu(self): + """ construct the left hand quiz menu """ + if self.quiz.discussion_list != []: # links for discussion items + discussion_list = '\n <ul>\n {}\n </ul>'.format( + '\n '.join( + webquiz_templates.discuss.format(b=q + 1, title=d.short_heading) + for (q, d) in enumerate(self.quiz.discussion_list))) + else: + discussion_list = '' + + # department and institution links + department = '''<a href="{0.department_url}">{0.department}</a>'''.format(self.quiz) + institution = '''<a href="{0.institution_url}">{0.institution}</a>'''.format(self.quiz) + + # question buttons + if self.number_questions == 0: + question_buttons = '' + else: + buttons = '\n' + '\n'.join( + webquiz_templates.button.format(b=q, + cls='button-selected' if self.quiz.discussion_list==[] and q==1 else 'blank' + ) + for q in range(1, self.number_questions + 1)) + question_buttons=webquiz_templates.question_buttons.format( + buttons=buttons, **self.language + ) + + # the full side menu + self.side_menu = webquiz_templates.side_menu.format( + discussion_list=discussion_list, + version=self.metadata.version, + department=department, + institution=institution, + side_questions=self.language['questions'] if self.number_questions>0 else '', + question_buttons=question_buttons, + copyright_years=self.metadata.copyright[:self.metadata.copyright.index(' ')], + **self.language) + + def add_question_javascript(self): + """ + Add the javascript for the questions to self and write the javascript + initialisation file, <quiz>/quiz_specs.js, for the quiz. When the quiz + page is loaded, WebQuizInit reads the quiz_specs initialisation file to + load the answers to the questions, and the headers for the discussion + items. We don't explicitly list quiz_specs.js in the meta data for the + quiz page because we want to hide this information from the student, + although they can easily get this if they open by the javascript + console and know what to look for. + """ + + try: + os.makedirs(self.quiz_name, exist_ok=True) + os.chmod(self.quiz_name, mode=0o755) + with codecs.open(os.path.join(self.quiz_name, 'wq-' + self.quiz_name + '.js'), 'w', + encoding='utf8') as quiz_specs: + if self.number_discussions > 0: + for (i, d) in enumerate(self.quiz.discussion_list): + quiz_specs.write('Discussion[{}]="{}";\n'.format(i+1, d.heading)) + if self.number_questions > 0: + for (i, question) in enumerate(self.quiz.question_list): + # QuizSpecifications is a 0-based array + quiz_specs.write('QuizSpecifications[{}]=[];\n'.format(i+1)) + quiz_specs.write('QuizSpecifications[{}].type="{}";\n'.format(i+1, question.type)) + if question.type == 'input': + quiz_specs.write('QuizSpecifications[{}].value="{}";\n'.format(i+1, + question.answer.lower() if question.comparison=='lowercase' + else question.answer + ) + ) + quiz_specs.write('QuizSpecifications[{}].comparison="{}";\n'.format(i+1, question.comparison)) + else: + quiz_specs.write(''.join( + 'QuizSpecifications[{}][{}]={};\n'.format(i+1, j, s.correct) + for (j, s) in enumerate(question.items) + ) + ) + + if self.quiz.hide_side_menu: + quiz_specs.write('toggle_side_menu();\n') + if self.quiz.one_page: + quiz_specs.write('onePage = true;\n') + if self.quiz.random_order: + quiz_specs.write('shuffleQuestions();\n') + quiz_specs.write('initSession();\n') + if self.number_discussions+self.number_questions>0: + quiz_specs.write('gotoQuestion({});'.format( + -1 if self.number_discussions>0 else 1) + ) + + except Exception as err: + self.webquiz_error('error writing quiz specifications', err) + + self.javascript = webquiz_templates.questions_javascript.format( + webquiz_url=self.webquiz_url, + mathjax=self.settings['mathjax'] + ) + self.webquiz_init = webquiz_templates.webquiz_init.format( + number_questions=self.number_questions, + number_discussions=self.number_discussions, + quiz_file=self.quiz_name, + ) + + def add_quiz_header_and_questions(self): + r''' + Write the quiz head and the main body of the quiz. + ''' + if self.quiz.one_page: + arrows = '' + else: + arrows = webquiz_templates.navigation_arrows.format( + question_number=self.quiz.discussion_list[0].heading + if self.quiz.discussion_list != [] + else '1' if self.quiz.question_list > [] + else '', + **self.language + ) + + # specify the quiz header - this will be wrapped in <div class="question-header>...</div> + self.quiz_header = webquiz_templates.quiz_header.format( + title=self.quiz.title, + arrows=arrows, + **self.language + ) + + # index for quiz + if self.quiz.quiz_index != []: + # add index to the web page + self.quiz_questions += webquiz_templates.quiz_index_div.format( + title=self.quiz.title if self.quiz.title!='' else self.quiz.unit_name, + quiz_index='\n '.join( + webquiz_templates.index_item.format( + url=q.url, + title='{} {}. {}'.format(self.language['quiz'],num+1,q.title) + if q.prompt else q.title, + ) for (num, q) in enumerate(self.quiz.quiz_index)), + **self.language) + # write a javascript file for displaying the menu + # quizmenu = the index file for the quizzes in this directory + with codecs.open('quizindex.js', 'w', encoding='utf8') as quizmenu: + quizmenu.write('var QuizTitles = [\n{titles}\n];\n'.format( + titles=',\n'.join(" ['{}', '{}']".format( + '{} {}. {}'.format(self.language['quiz'],num+1,q.title) + if q.prompt else q.title, + q.url) + for (num,q) in enumerate(self.quiz.quiz_index)) + ) + ) + quizmenu.write(webquiz_templates.create_quizindex_menu) + + # now comes the main page text + # discussion(s) masquerade as negative questions + if self.quiz.discussion_list != []: + dnum = 0 + for d in self.quiz.discussion_list: + dnum += 1 + self.quiz_questions += webquiz_templates.discussion.format( + dnum=dnum, + discussion=d, + display='inline' if self.quiz.one_page else 'none', + heading=webquiz_templates.discussion_heading.format(d.heading) + if self.quiz.one_page else '' + ) + + # finally we print the questions + if self.quiz.question_list != []: + self.quiz_questions += ''.join( + webquiz_templates.question_wrapper.format( + qnum=qnum + 1, + question_number='{} {}. '.format(self.language.question, qnum+1) + if self.quiz.one_page else '', + display='inline' if self.quiz.one_page else 'none', + question=self.print_question(quiz_question, qnum + 1), + feedback=self.print_feedback(quiz_question, qnum + 1) + ) + for (qnum, quiz_question) in enumerate(self.quiz.question_list)) + + def print_question(self, question, qnum): + r'''Here: + - question is the question + - qnum is the number of the question + ''' + if question.type == 'input': + self.webquiz_debug('Q{}: after_text={}.'.format(qnum, question.after_text)) + question_options = webquiz_templates.input_answer.format( + size=5+len('{}'.format(question.answer)), + after_text=question.after_text, + qnum=qnum, + answer=self.language['answer']+':' if question.prompt else '' + ) + elif question.type in ['single', 'multiple']: + question_options = webquiz_templates.choice_answer.format( + after_text=question.after_text, + choices='\n'.join(self.print_choices(qnum, question, choice) + for choice in range(len(question.items))) + ) + else: + self.webquiz_error('Unknown question type "{}" in question {}'.format(question.type, qnum)) + return webquiz_templates.question_text.format( + qnum=qnum, + question_text=question.text, + nextquestion='' if self.quiz.one_page else webquiz_templates.nextquestion.format(**self.language), + question_options=question_options, + **self.language) + + def print_choices(self, qnum, question, part): + r''' + Here: + - qnum = question number + - question = current question + - part = number of the option we need to process. + We put the parts into question.columns multicolumn format, so we have + to add '<tr>' and '</tr>' tags depending on part. + ''' + choice = question.items[part] + item = '<tr>' if question.columns == 1 or (part % question.columns) == 0 else '<td> </td>' + if question.type == 'single': + item += webquiz_templates.single_item.format(choice=choice.symbol, qnum=qnum, text=choice.text) + elif question.type == 'multiple': + item += webquiz_templates.multiple_item.format( + choice=choice.symbol, + qnum=qnum, + optnum=part, + text=choice.text + ) + else: + item += '<!-- internal error: %s -->\n' % question.type + self.webquiz_error('Unknown question type "{}" in question {}'.format(question.type, qnum)) + + if question.columns == 1 or (part+1) % question.columns == 0 or part == len(question.items) - 1: + item += ' </tr><!-- part={}, cols={}, # answers = {} -->\n'.format( + part, question.columns, len(question.items)) + return item + + def print_feedback(self, question, qnum): + r''' + Generate the HTML for displaying the feedback help text when the user + answers a question. + ''' + if question.type == 'input': + feedback = webquiz_templates.tf_feedback_text.format( + choice=qnum, + feedback='true', + correct_answer=self.language.correct, + answer2='', + text=question.feedback_right) + feedback += webquiz_templates.tf_feedback_text.format( + choice=qnum, + feedback='false', + correct_answer=self.language.incorrect, + answer2=self.language.try_again, + text=question.feedback_wrong) + elif question.type == "single": + feedback = '\n' + '\n'.join( + webquiz_templates.single_feedback.format( + qnum=qnum, + part=snum + 1, + correct_answer=self.language.correct if s.correct == 'true' else self.language.incorrect, + alpha_choice=self.language.choice.format(s.symbol), + feedback=s.feedback, + **self.language) + for (snum, s) in enumerate(question.items)) + elif question.type == "multiple": + feedback = '\n' + '\n'.join(webquiz_templates.multiple_feedback.format( + qnum=qnum, + part=snum + 1, + correct_answer=getattr(self.language, s.correct).capitalize(), + feedback=s.feedback, + multiple_choice_opener=self.language.multiple_incorrect. + format(s.symbol), + **self.language) + for (snum, s) in enumerate(question.items) + ) + feedback += webquiz_templates.multiple_feedback_correct.format( + qnum=qnum, + feedback='\n'.join(webquiz_templates.multiple_feedback_answer.format( + correct_answer=getattr(self.language, s.correct).capitalize(), + reason=s.feedback) for s in question.items), + **self.language) + else: + self.webquiz_error('Unknown question type "{}" in question {}'.format(question.type, qnum)) + + return '<div class="answer">' + feedback + '</div>' |