''' Parsing and support functions for the Moodle GIFT format. ''' from pyparsing import (Word, Literal, Optional, Group, OneOrMore, ParseException, Combine, restOfLine, nums, StringEnd, ZeroOrMore, oneOf, originalTextFor, CharsNotIn, NotAny, ParserElement, printables, White, FollowedBy, delimitedList) from quizdata.question import (MultipleChoiceQuestion, MissingWordQuestion, TrueFalseQuestion, ShortAnswerQuestion, NumericalQuestion, MatchingQuestion) __all__ = [ 'parse', 'question', 'questions' ] ParserElement.enablePackrat() ParserElement.setDefaultWhitespaceChars(" \t") comment = Literal("//") + restOfLine NL = Literal("\n").suppress() def kill_whitespace(s, l, t): #print t.asList()[1:-1] return [ ' '.join(t.asList()[1:-1]).strip() ] def printables_except(chars): l = list(printables) for c in chars: l.remove(c) return ''.join(l) def restricted_text(allowed_chars): return originalTextFor( Optional(NL) + OneOrMore((Word(allowed_chars) + Optional(NL))) ).setParseAction( kill_whitespace ) text = restricted_text(printables) qtext = restricted_text(printables_except("{")) ans_text = restricted_text(printables_except("=~#}->")) title_text = restricted_text(printables_except(":")) format_text = restricted_text(printables_except("]")) number = Combine(Word(nums) + Optional("." + Optional(Word(nums)))) dcolon = Literal("::").suppress() qtitle = dcolon + title_text + Optional(NL) + dcolon + Optional(NL) qformat = Literal("[").suppress() + format_text + Literal("]").suppress() + Optional(NL) correct_ans = (Literal("=") + ans_text)("correct_text") wrong_ans = Literal("~") + ans_text("wrong_text") ans_explain = Literal("#") + ans_text("explain_text") ans_tf = oneOf("T F TRUE FALSE")("tf_text") ans_match = Literal("=") + ans_text("left_match") + Literal("->") + ans_text("right_match") ans_numeric = Group(Optional(Literal("=")) + (number("number") + Optional( Literal(":") + number("range")) ^number("beg_range") + Literal("..") + number("end_range")) + Optional( ans_explain )("explain")) + Optional(NL) answer = Group(( ( correct_ans ^ wrong_ans ^ ans_tf )("ans_text") + Optional(ans_explain)("explain")) ^ ans_match("matching_answer") ) + Optional(NL) answer_list = Group((Literal("{").suppress() + Optional(NL) + OneOrMore(answer) + Literal("}").suppress()) ^ (Literal("{#").suppress() + Optional(NL) + OneOrMore(ans_numeric)("numeric_ans") + Literal("}").suppress())) question = ZeroOrMore(NL) + Group( Optional(qformat)("format") + Optional(qtitle)("title") + qtext("text") + answer_list("answers") + Optional(text)("text_additional") ) question.ignore(comment) questions = delimitedList(question, delim=OneOrMore(NL))("questions") + ZeroOrMore(NL) + StringEnd() questions.ignore(comment) def _search_file(parser, stream): return parser.searchString(stream.read()) def parse(stream, params): parsed = _search_file(question, stream) ret = [] for q in parsed: ques = _question_maker(q[0]) if ques: ret.append(ques) return ret def _question_maker(q): answers = q['answers'][0] try: if 'text_additional' in q: #XXX This really a different question type? return _missing_word_question_maker(q) elif any('tf_text' in answer.keys() for answer in answers if hasattr(answer, 'keys')): return _true_false_question_maker(q) elif any('numeric_ans' in answer.keys() for answer in answers if hasattr(answer, 'keys')): print "XXX: SKIPPING NUMERICAL QUESTION!" elif any("wrong_text" not in answer.keys() for answer in answers if hasattr(answer, 'keys')): return _short_answer_question_maker(q) elif all("left_match" in answer.keys() for answer in answers if hasattr(answer, 'keys')): _matching_question_maker(q) elif len(answers) > 1: return _multi_choice_question_maker(q) else: print ("XXX: Couldn't identify question type!") print q return None except Exception: import traceback print "Error converting parsed format into question object!" print zip(q.keys(), (str(v) for v in q.values())) print q traceback.print_exc() return None def _missing_word_question_maker(q): answers = _make_answers(q['answers']) correct = _get_correct_answer(answers) params = { 'markup_type': MARKUP_TYPES[q['format']] if 'format' in q else unicode, 'tail_text': str(q['text_additional'][0]), 'text' : str(q['text'][0]), 'answers' : answers, 'correct' : correct } if 'title' in q: params['title'] = q['title'][0] return MissingWordQuestion(**params) def _short_answer_question_maker(q): answers = _make_answers(q['answers']) correct = _get_correct_answer(answers) params = { 'text': str(q['text'][0]), 'markup_type': MARKUP_TYPES[q['format']] if 'format' in q else unicode, 'correct' : correct } if 'title' in q: params['title'] = q['title'][0] return ShortAnswerQuestion(**params) def _matching_question_maker(q): answers = _make_answers(q['answers']) params = { 'text': str(q['text'][0]), 'markup_type': MARKUP_TYPES[q['format']] if 'format' in q else unicode, 'answers' : answers, } if 'title' in q: params['title'] = str(q['title'][0]) return MatchingQuestion(**params) def _true_false_question_maker(q): params = { 'text': str(q['text'][0]), 'markup_type': MARKUP_TYPES[q['format']] if 'format' in q else unicode, 'correct' : q['answers'][0][0] } if 'title' in q: params['title'] = q['title'][0] return TrueFalseQuestion(**params) def _multi_choice_question_maker(q): answers = _make_answers(q['answers']) correct = _get_correct_answer(answers) params = { 'text': str(q['text'][0]), 'markup_type': MARKUP_TYPES[q['format']] if 'format' in q else unicode, 'answers': answers, 'correct': correct, } if 'title' in q: params['title'] = q['title'][0] return MultipleChoiceQuestion(**params) class AnswerList(list): pass def _make_answers(a): if all(['left_match'] in answer.keys() for answer in a): return _make_matching_answers(a) ret = AnswerList() for i in a: ret.append(str(i[1])) if i[0] == '=': ret.correct = i[1] return ret def _make_matching_answers(a): ret = AnswerList() for i in a[0]: ret.append((str(i['left_match']), str(i['right_match']))) return ret def _get_correct_answer(answers): #XXX: needs help... for a in answers: if a[0] == '=': return a