def run(self): env = self.state.document.settings.env if not 'config' in self.options: raise SphinxError('Config option is required') import os path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format(self.options['config'])) item_list = yaml_writer.read(path) itemnodes = [] for item in item_list: title, info, img = [item.get(u"title", u""), item.get(u"info", u""), item.get(u"image_url", u"")] _, node, data = self.create_question(title_text=self.arguments[0].replace(u"$title", title), points=False) more = u"" if img: e = aplus_nodes.html(u"p", {u"class": u"indent"}) e.append(aplus_nodes.html(u"img", {u"src":img, u"alt": title, u"style": u"max-height:100px;"})) node.append(e) more += str(e) if info: e = aplus_nodes.html(u"p", {u"class": u"indent"}) e.append(nodes.Text(info)) node.append(e) more += str(e) data[u'options'] = self.generate_options(env, node) data[u'more'] = more itemnodes.append(node) return itemnodes
def create_question(self, title_text=None, points=True): env = self.state.document.settings.env env.question_count += 1 # Create base element and data. node = aplus_nodes.html('div', { 'class': ' '.join(self.get_classes()), }) data = { 'type': self.grader_field_type(), 'extra_info': self.get_extra_info(), } key = self.options.get('key', None) if key: data['key'] = key # Add title. if not title_text is None: data['title'] = title_text elif env.questionnaire_is_feedback: data['title'] = title_text = '' else: # "#" in the question title is converted to a number in the MOOC-grader. # The questions of a "pick randomly" questionnaire should be numbered # in the MOOC-grader since they are randomly selected. postfix = '{#}' if env.aplus_pick_randomly_quiz else "{:d}".format( env.question_count) data['title|i18n'] = translations.opt('question', postfix=postfix) title_text = "{} {:d}".format(translations.get(env, 'question'), env.question_count) if title_text: title = aplus_nodes.html('label', {}) title.append(nodes.Text(title_text)) node.append(title) # Add configuration. if points and len(self.arguments) > 0: question_points = int(self.arguments[0]) data['points'] = question_points env.aplus_quiz_total_points += question_points if env.aplus_pick_randomly_quiz: if env.aplus_single_question_points is None: env.aplus_single_question_points = question_points else: if env.aplus_single_question_points != question_points: source, line = self.state_machine.get_source_and_line( self.lineno) logger.warning( "Each question must have equal points when " "the questionnaire uses the 'pick randomly' option.", location=(source, line)) if 'required' in self.options: data['required'] = True node.set_yaml(data, 'question') return env, node, data
def create_question(self, title_text=None, points=True): env = self.state.document.settings.env env.question_count += 1 # Create base element and data. node = aplus_nodes.html(u'div', { u'class': u' '.join(self.get_classes()), }) data = { u'type': self.grader_field_type(), u'extra_info': self.get_extra_info(), } key = self.options.get('key', None) if key: data[u'key'] = yaml_writer.ensure_unicode(key) # Add title. if not title_text is None: data[u'title'] = title_text elif env.questionnaire_is_feedback: data[u'title'] = title_text = u'' else: data[u'title|i18n'] = translations.opt('question', postfix=u" {:d}".format( env.question_count)) title_text = u"{} {:d}".format(translations.get(env, 'question'), env.question_count) if title_text: title = aplus_nodes.html(u'label', {}) title.append(nodes.Text(title_text)) node.append(title) # Add configuration. if points and len(self.arguments) > 0: question_points = int(self.arguments[0]) data['points'] = question_points env.aplus_quiz_total_points += question_points if env.aplus_pick_randomly_quiz: if env.aplus_single_question_points is None: env.aplus_single_question_points = question_points else: if env.aplus_single_question_points != question_points: source, line = self.state_machine.get_source_and_line( self.lineno) logger.warning( "Each question must have equal points when " "the questionnaire uses the 'pick randomly' option.", location=(source, line)) if 'required' in self.options: data[u'required'] = True node.set_yaml(data, u'question') return env, node, data
def run(self): if len(self.arguments) > 0: key = self.arguments[0] else: raise SphinxError('Missing active element input id') env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [] if 'class' in self.options: classes.extend(self.options['class']) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-active-element': u'in', u'id': u'' + key, } if 'title' in self.options: args['data-title'] = self.options['title'] if 'default' in self.options: args['data-default'] = self.options['default'] if 'type' in self.options: args['data-type'] = self.options['type'] if 'width' in self.options: args['style'] = 'width:' + self.options['width'] + ';' if 'height' in self.options: if 'style' not in args: args['style'] = 'height:' + self.options['height'] + ';' else: args['style'] = args['style'] + 'height:' + self.options[ 'height'] + ';' if 'clear' in self.options: args['style'] = args['style'] + 'clear:' + self.options[ 'clear'] + ';' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append( nodes.Text(translations.get(env, 'active_element_placeholder'))) node.append(paragraph) return [node]
def run(self): self.assert_has_content() node = nodes.container() name = self.arguments[0] # TODO: maybe use sphinx/docutils node instead of aplus_node for the link link = aplus_nodes.html(u'a', { u'href':u'#' + name, u'data-toggle':u'collapse'}) label = 'Show/Hide' if 'label' in self.options: label = self.options['label'] # Container for the hidden content classes = ['collapse'] if 'visible' in self.options: classes.append('in') collapsible = nodes.container('\n'.join(self.content)) collapsible['classes'].extend(classes) self.options['name'] = name self.add_name(collapsible) link.append(nodes.Text(label)) node.append(link) node.append(collapsible) self.state.nested_parse(self.content, self.content_offset, collapsible) return [node]
def run(self): self.assert_has_content() node = nodes.container() name = self.arguments[0] # TODO: maybe use sphinx/docutils node instead of aplus_node for the link link = aplus_nodes.html('a', { 'href':'#' + name, 'data-toggle':'collapse'}) label = 'Show/Hide' if 'label' in self.options: label = self.options['label'] # Container for the hidden content classes = ['collapse'] if 'visible' in self.options: classes.append('in') collapsible = nodes.container('\n'.join(self.content)) collapsible['classes'].extend(classes) self.options['name'] = name self.add_name(collapsible) link.append(nodes.Text(label)) node.append(link) node.append(collapsible) self.state.nested_parse(self.content, self.content_offset, collapsible) return [node]
def run(self): # This directive is obsolete, AgreeItems can be placed alone. classes = [u'form-group'] if 'class' in self.options: classes.extend(self.options['class']) node = aplus_nodes.html(u'div', { u'class': u" ".join(classes) }) nested_parse_with_titles(self.state, self.content, node) return [node]
def run(self): # This directive is obsolete, AgreeItems can be placed alone. classes = [u'form-group'] if 'class' in self.options: classes.extend(self.options['class']) node = aplus_nodes.html(u'div', {u'class': u" ".join(classes)}) nested_parse_with_titles(self.state, self.content, node) return [node]
def add_feedback(self, node, data, paragraph): if not paragraph: return # Add feedback node for rendering without writing to file. data['feedback'] = ('#!children', 'feedback') feedbacks = aplus_nodes.html('p', {'class': 'feedback-holder'}, no_write=True) for i, line in slicer(paragraph): if not '§' in line[0]: raise SphinxError('Feedback separator § exptected: {}'.format( line[0])) value, content = line[0].split('§', 1) value = value.strip() line[0] = content.strip() isnot = False compare_regexp = False if value.startswith('!'): isnot = True value = value[1:] if value.startswith('regexp:'): compare_regexp = True value = value[7:] # Create document elements. hint = aplus_nodes.html('div') text = aplus_nodes.html('p', {}) text.store_html('hint') nested_parse_with_titles(self.state, line, text) hint.append(text) feedbacks.append(hint) # Add configuration data. fbdata = { 'value': value, 'label': ('#!html', 'hint'), } if isnot: fbdata['not'] = True if compare_regexp: fbdata['compare_regexp'] = True hint.set_yaml(fbdata, 'feedback') node.append(feedbacks)
def generate_options(self, env, node): options = [] for i, key in enumerate(['agreement4', 'agreement3', 'agreement2', 'agreement1', 'agreement0']): options.append({ u'value': 4 - i, u'label|i18n': translations.opt(key), }) choice = aplus_nodes.html(u'div', {u'class':u'radio'}) label = aplus_nodes.html(u'label', {}) label.append(aplus_nodes.html(u'input', { u'type': u'radio', u'name': u'field_{:d}'.format(env.question_count - 1), u'value': 4 - i, })) label.append(nodes.Text(translations.get(env, key))) choice.append(label) node.append(choice) return options
def add_instructions(self, node, data, plain_content): if not plain_content: return parent = aplus_nodes.html(u'div', {}) parent.store_html(u'more') nested_parse_with_titles(self.state, plain_content, parent) node.append(parent) data[u'more'] = (u'#!html', u'more')
def run(self): self.assert_has_content() container_class = 'row' row_container_opts = { 'class': container_class, } row_container = aplus_nodes.html('div', row_container_opts) self.state.nested_parse(self.content, self.content_offset, row_container) return [row_container]
def create_question(self, title_text=None, points=True): env = self.state.document.settings.env env.question_count += 1 # Create base element and data. node = aplus_nodes.html(u'div', { u'class': u' '.join(self.get_classes()), }) data = { u'type': self.grader_field_type(), u'extra_info': self.get_extra_info(), } key = self.options.get('key', None) if key: data[u'key'] = yaml_writer.ensure_unicode(key) # Add title. if not title_text is None: data[u'title'] = title_text elif env.questionnaire_is_feedback: data[u'title'] = title_text = u'' else: data[u'title|i18n'] = translations.opt('question', postfix=u" {:d}".format( env.question_count)) title_text = u"{} {:d}".format(translations.get(env, 'question'), env.question_count) if title_text: title = aplus_nodes.html(u'label', {}) title.append(nodes.Text(title_text)) node.append(title) # Add configuration. if points and len(self.arguments) > 0: env.aplus_quiz_total_points += int(self.arguments[0]) data[u'points'] = int(self.arguments[0]) if 'required' in self.options: data[u'required'] = True node.set_yaml(data, u'question') return env, node, data
def add_feedback(self, node, data, paragraph): if not paragraph: return # Add feedback node for rendering without writing to file. data[u'feedback'] = (u'#!children', u'feedback') feedbacks = aplus_nodes.html(u'p', {u'class':u'feedback-holder'}, no_write=True) for i,line in slicer(paragraph): if not u'§' in line[0]: raise SphinxError(u'Feedback separator § exptected: {}'.format(line[0])) value,content = line[0].split(u'§', 1) value = value.strip() line[0] = content.strip() isnot = False if value.startswith(u'!'): isnot = True value = value[1:] # Create document elements. hint = aplus_nodes.html(u'div') text = aplus_nodes.html(u'p', {}) text.store_html('hint') nested_parse_with_titles(self.state, line, text) hint.append(text) feedbacks.append(hint) # Add configuration data. fbdata = { u'value': value, u'label': (u'#!html', u'hint'), } if isnot: fbdata[u'not'] = True hint.set_yaml(fbdata, u'feedback') node.append(feedbacks)
def create_question(self, title_text=None, points=True): env = self.state.document.settings.env env.question_count += 1 # Create base element and data. node = aplus_nodes.html(u'div', { u'class': u' '.join(self.get_classes()), }) data = { u'type': self.grader_field_type(), u'extra_info': self.get_extra_info(), } key = self.options.get('key', None) if key: data[u'key'] = yaml_writer.ensure_unicode(key) # Add title. if not title_text is None: data[u'title'] = title_text elif env.questionnaire_is_feedback: data[u'title'] = title_text = u'' else: data[u'title|i18n'] = translations.opt('question', postfix=u" {:d}".format(env.question_count)) title_text = u"{} {:d}".format(translations.get(env, 'question'), env.question_count) if title_text: title = aplus_nodes.html(u'label', {}) title.append(nodes.Text(title_text)) node.append(title) # Add configuration. if points and len(self.arguments) > 0: data[u'points'] = int(self.arguments[0]) if 'required' in self.options: data[u'required'] = True node.set_yaml(data, u'question') return env, node, data
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env errors = [] if not 'target_category' in self.options: errors.append('Error in {} directive: Missing target_category.'.format(self.name)) if not 'target_url' in self.options: errors.append('Error in {} directive: Missing target_url.'.format(self.name)) assert not errors, '\n'.join(errors) name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) node = aplus_nodes.html(u'div',{ u'class': 'exercisecollection', u'data-aplus-exercise': u'yes', }) data = { u'key': name, u'category': self.options.get('category', 'prerequisit'), u'max_points': self.options.get('max_points', 10), u'points_to_pass': self.options.get('points-to-pass', 0), u'target_url': self.options.get('target_url', None), u'target_category': self.options.get('target_category', None), u'title': self.arguments[0] } node.write_yaml(env, name, data, 'exercisecollection') return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env errors = [] if not 'target_category' in self.options: errors.append( 'Error in {} directive: Missing target_category.'.format( self.name)) if not 'target_url' in self.options: errors.append('Error in {} directive: Missing target_url.'.format( self.name)) assert not errors, '\n'.join(errors) name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) node = aplus_nodes.html(u'div', { u'class': 'exercisecollection', u'data-aplus-exercise': u'yes', }) data = { u'key': name, u'category': self.options.get('category', 'prerequisit'), u'max_points': self.options.get('max_points', 10), u'points_to_pass': self.options.get('points-to-pass', 0), u'target_url': self.options.get('target_url', None), u'target_category': self.options.get('target_category', None), u'title': self.arguments[0] } node.write_yaml(env, name, data, 'exercisecollection') return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env errors = [] for option in ['collection_category', 'max_points']: if option not in self.options: # Because warning, error and severe don't make any difference. errors.append( 'Error in {} directive: Missing option {}.'.format( self.name, option)) #self.severe('Error in {} directive: Missing option {}.'.format(self.name, option)) if ('collection_course' in self.options) == ('collection_url' in self.options): errors.append( 'Error in {} directive: Must contain either "collection_course" or "collection_url" option.' .format(self.name)) if ('collection_course' in self.options) and (not ';' in str( self.options.get('collection_course'))): # Because warning, error and severe don't make any difference. errors.append( 'Error in {} directive: collection_course must be in format "<course>;<instance>"' .format(self.name)) #self.reporter.severe('Error in {} directive: collection_course must be in format "<course>;<instance>".'.format(self.name)) if errors: assert False, '\n'.join(errors) if 'max_points' in self.options: points = int(self.options.get('max_points')) else: points = 10 if 'category' in self.options: category = str(self.options.get('category')) else: category = 'Prerequisit' name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) node = aplus_nodes.html(u'div', { u'class': 'exercisecollection', u'data-aplus-exercise': u'yes', }) data = { u'key': name, u'category': category, u'max_points': points, u'points_to_pass': self.options.get('points-to-pass'), u'collection_course': None, u'collection_url': None, u'collection_category': self.options.get('collection_category'), } if 'collection_course' in self.options: data[u'collection_course'] = self.options.get('collection_course') else: data[u'collection_url'] = self.options.get('collection_url') node.write_yaml(env, name, data, 'exercisecollection') return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = "{}_{}".format(env.docname.replace('/', '_'), key) override = env.config.override classes = ['exercise'] if 'class' in self.options: classes.extend(self.options['class']) if difficulty: classes.append('difficulty-' + difficulty) # Add document nodes. args = { 'class': ' '.join(classes), 'data-aplus-exercise': 'yes', } if 'quiz' in self.options: args['data-aplus-quiz'] = 'yes' if 'ajax' in self.options: args['data-aplus-ajax'] = 'yes' node = aplus_nodes.html('div', args) key_title = "{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format( self.options['config'])) data = yaml_writer.read(path) config_title = data.get('title', '') else: data = {'_external': True} if 'url' in self.options: data['url'] = self.options['url'] if 'lti' in self.options: data.update({ 'lti': self.options['lti'], 'lti_context_id': self.options.get('lti_context_id', ''), 'lti_resource_link_id': self.options.get('lti_resource_link_id', ''), }) if 'lti_aplus_get_and_post' in self.options: data.update({'lti_aplus_get_and_post': True}) if 'lti_open_in_iframe' in self.options: data.update({'lti_open_in_iframe': True}) config_title = '' config_title = self.options.get('title', config_title) if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options: data['radar_info'] = { 'tokenizer': self.options.get("radar_tokenizer"), 'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"), } category = 'submit' data.update({ 'key': name, 'category': 'submit', 'scale_points': points, 'difficulty': difficulty or '', 'max_submissions': self.options.get( 'submissions', data.get('max_submissions', env.config.program_default_submissions)), 'min_group_size': data.get('min_group_size', env.config.default_min_group_size), 'max_group_size': data.get('max_group_size', env.config.default_max_group_size), 'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)), # The RST source file path is needed for fixing relative URLs # in the exercise description. # Replace the Windows path separator backslash \ with the Unix forward slash /. '_rst_srcpath': env.doc2path(env.docname, None).replace('\\', '/'), }) self.set_assistant_permissions(data) if data.get('title|i18n'): # Exercise config.yaml defines title|i18n for multiple languages. # Do not write the field "title" to data in order to avoid conflicts. if config_title: # Overwrite the title for one language since the RST directive # has defined the title option (or alternatively, the yaml file # has "title" in addition to "title|i18n", but that does not make sense). # env.config.language may be incorrect if the language can not be detected. data['title|i18n'][ env.config.language] = env.config.submit_title.format( key_title=key_title, config_title=config_title) else: formatted_title = env.config.submit_title.format( key_title=key_title, config_title=config_title) # If no title has been defined, use key_title as the default. data['title'] = formatted_title if formatted_title else key_title if self.content: self.assert_has_content() # Sphinx can not compile the nested RST into HTML at this stage, hence # the HTML instructions defined in this directive body are added to # the exercise YAML file only at the end of the build. Sphinx calls # the visit functions of the nodes in the last writing phase. # The instructions are added to the YAML file in the depart_html # function in aplus_nodes.py. exercise_description = aplus_nodes.html('div', {}) exercise_description.store_html('exercise_description') nested_parse_with_titles(self.state, self.content, exercise_description) node.append(exercise_description) data['instructions'] = ('#!html', 'exercise_description') else: # The placeholder text is only used in the built HTML # (not in the YAML configurations). paragraph = aplus_nodes.html('p', {}) paragraph.append( nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) data.setdefault('status', self.options.get('status', 'unlisted')) source, line = self.state_machine.get_source_and_line(self.lineno) if 'reveal-submission-feedback' in self.options: data['reveal_submission_feedback'] = parse_reveal_rule( self.options['reveal-submission-feedback'], source, line, 'reveal-submission-feedback', ) if 'reveal-model-solutions' in self.options: data['reveal_model_solutions'] = parse_reveal_rule( self.options['reveal-model-solutions'], source, line, 'reveal-model-solutions', ) if 'grading-mode' in self.options: data['grading_mode'] = self.options['grading-mode'] if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if 'category' in self.options: data['category'] = str(self.options['category']) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() key, difficulty, points = self.extract_exercise_arguments() # Parse options. classes = ['exercise'] is_feedback = False if 'chapter-feedback' in self.options: classes.append('chapter-feedback') is_feedback = True if 'weekly-feedback' in self.options: classes.append('weekly-feedback') is_feedback = True if 'appendix-feedback' in self.options: classes.append('appendix-feedback') is_feedback = True if 'course-feedback' in self.options: classes.append('course-feedback-questionnaire') is_feedback = True if 'feedback' in self.options: is_feedback = True if is_feedback: key = 'feedback' category = 'feedback' classes.append('feedback') else: category = 'questionnaire' if difficulty: classes.append('difficulty-' + difficulty) if 'category' in self.options: category = str(self.options.get('category')) env = self.state.document.settings.env name = "{}_{}".format(env.docname.replace('/', '_'), key) override = env.config.override env.questionnaire_is_feedback = is_feedback env.question_count = 0 env.aplus_single_question_points = None env.aplus_quiz_total_points = 0 env.aplus_pick_randomly_quiz = 'pick_randomly' in self.options env.aplus_random_question_exists = False # Create document elements. node = aplus_nodes.html('div', { 'class': ' '.join(classes), 'data-aplus-exercise': 'yes', }) form = aplus_nodes.html('form', { 'action': key, 'method': 'post', }) nested_parse_with_titles(self.state, self.content, form) submit = aplus_nodes.html('input', { 'type': 'submit', 'value': translations.get(env, 'submit'), 'class': 'btn btn-primary', }, skip_html=True) form.append(submit) node.append(form) # Write configuration file. data = { 'key': name, 'category': category, 'difficulty': difficulty or '', 'max_submissions': self.options.get( 'submissions', 0 if is_feedback else env.config.questionnaire_default_submissions), 'min_group_size': 1 if is_feedback else env.config.default_min_group_size, 'max_group_size': 1 if is_feedback else env.config.default_max_group_size, 'points_to_pass': self.options.get('points-to-pass', 0), 'feedback': is_feedback, 'view_type': 'access.types.stdsync.createForm', 'status': self.options.get('status', 'unlisted'), 'fieldgroups': [{ 'title': '', 'fields': ('#!children', None), }], # The RST source file path is needed for fixing relative URLs # in the exercise description. # Replace the Windows path separator backslash \ with the Unix forward slash /. '_rst_srcpath': env.doc2path(env.docname, None).replace('\\', '/'), } meta_data = env.metadata[env.app.config.master_doc] # Show the model answer after the last submission. if 'reveal-model-at-max-submissions' in self.options: data['reveal_model_at_max_submissions'] = str_to_bool( self.options['reveal-model-at-max-submissions']) else: default_reveal = str_to_bool( meta_data.get( 'questionnaire-default-reveal-model-at-max-submissions', 'false'), error_msg_prefix=env.app.config.master_doc + " questionnaire-default-reveal-model-at-max-submissions: ") if default_reveal: data['reveal_model_at_max_submissions'] = default_reveal # Show the model answer after the module deadline. if 'show-model' in self.options: data['show_model_answer'] = str_to_bool(self.options['show-model']) else: show_default = str_to_bool( meta_data.get('questionnaire-default-show-model', 'true'), error_msg_prefix=env.app.config.master_doc + " questionnaire-default-show-model: ") if not show_default: data['show_model_answer'] = show_default if env.aplus_pick_randomly_quiz: pick_randomly = self.options.get('pick_randomly', 0) if pick_randomly < 1: source, line = self.state_machine.get_source_and_line( self.lineno) raise SphinxError( source + ": line " + str(line) + "\nNumber of fields to sample randomly should be greater than zero " "(option pick_randomly in the questionnaire directive).") data['fieldgroups'][0]['pick_randomly'] = pick_randomly if 'preserve-questions-between-attempts' in self.options: data['fieldgroups'][0]['resample_after_attempt'] = False elif not env.aplus_random_question_exists: # The HTML attribute data-aplus-quiz makes the A+ frontend show the # questionnaire feedback in place of the exercise description once # the student has submitted at least once. In randomized questionnaires, # the same form may not be submitted again due to one-time use nonce # values, hence the attribute must not be used in randomized # questionnaires. node.attributes['data-aplus-quiz'] = 'yes' if 'autosave' in self.options or env.config.enable_autosave: node.attributes['data-aplus-autosave'] = 'yes' self.set_assistant_permissions(data) points_set_in_arguments = len( self.arguments) == 2 and difficulty != self.arguments[1] if env.aplus_pick_randomly_quiz: calculated_max_points = (self.options.get('pick_randomly') * env.aplus_single_question_points if env.aplus_single_question_points is not None else 0) else: calculated_max_points = env.aplus_quiz_total_points if calculated_max_points == 0 and is_feedback: data['max_points'] = points else: if points_set_in_arguments and calculated_max_points != points: source, line = self.state_machine.get_source_and_line( self.lineno) raise SphinxError( source + ": line " + str(line) + "\nThe points of the questions in the questionnaire must add up to the total points of the questionnaire!" ) data['max_points'] = calculated_max_points if 'title' in self.options: data['title'] = self.options.get('title') else: data['title|i18n'] = translations.opt( 'feedback') if is_feedback else translations.opt( 'exercise', postfix=" {}".format(key)) source, line = self.state_machine.get_source_and_line(self.lineno) if 'reveal-submission-feedback' in self.options: data['reveal_submission_feedback'] = parse_reveal_rule( self.options['reveal-submission-feedback'], source, line, 'reveal-submission-feedback', ) if 'reveal-model-solutions' in self.options: data['reveal_model_solutions'] = parse_reveal_rule( self.options['reveal-model-solutions'], source, line, 'reveal-model-solutions', ) if 'grading-mode' in self.options: data['grading_mode'] = self.options['grading-mode'] if not 'no-override' in self.options and category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) form.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() # Detect paragraphs: any number of plain content, choices and optional feedback. empty_lines = list(loc for loc, line in enumerate(self.content) if line == '') plain_content = None choices = [] feedback = [] if len(empty_lines) > 0: last = self.content[(empty_lines[-1] + 1):] def split_second_last(empty_lines): if len(empty_lines) > 1: return self.content[:empty_lines[-2]], self.content[( empty_lines[-2] + 1):empty_lines[-1]] else: return None, self.content[:empty_lines[-1]] # Backwards compatibility for skipping feedback paragraph. if len(last) == 1 and last[0].startswith( 'I hereby declare that no feedback '): plain_content, choices = split_second_last(empty_lines) elif all('§' in line for line in last): plain_content, choices = split_second_last(empty_lines) feedback = last else: plain_content = self.content[:empty_lines[-1]] choices = last else: choices = self.content # Create question. env, node, data = self.create_question() self.add_instructions(node, data, plain_content) data['options'] = ('#!children', 'option') if 'partial-points' in self.options: data['partial_points'] = True dropdown = None if self.grader_field_type() == 'dropdown': # The HTML select element has a different structure compared # to the input elements (radio buttons and checkboxes). dropdown = aplus_nodes.html( 'select', { 'name': 'field_{:d}'.format(env.question_count - 1), }) correct_count = 0 # Travel all answer options. for i, line in slicer(choices): # Split choice key off. key, content = line[0].split(' ', 1) key = key.strip() line[0] = content.strip() # Trim the key. correct = False selected = False if key.startswith('+'): selected = True key = key[1:] if key.startswith('*'): correct = True key = key[1:] correct_count += 1 elif key.startswith('?'): correct = "neutral" key = key[1:] if key.endswith('.'): key = key[:-1] # Add YAML configuration data. optdata = { 'value': key, } if correct: optdata['correct'] = correct if selected: optdata['selected'] = True # Create document elements. if dropdown is None: # One answer alternative as a radio button or a checkbox. choice = aplus_nodes.html('div', {'class': 'radio'}) label = aplus_nodes.html('label', {}) attrs = { 'type': self.input_type(), 'name': 'field_{:d}'.format(env.question_count - 1), 'value': key, } if selected: attrs['checked'] = 'checked' label.append(aplus_nodes.html('input', attrs)) choice.append(label) node.append(choice) text = aplus_nodes.html('span', {}) text.store_html('label') nested_parse_with_titles(self.state, line, text) label.append(text) optdata['label'] = ('#!html', 'label') choice.set_yaml(optdata, 'option') else: # Add option elements to the select. # Options may only contain plain text, not formatted HTML. attrs = { 'value': key, } if selected: attrs['selected'] = 'selected' option = aplus_nodes.html('option', attrs) text = line[0] option.append(nodes.Text(text)) dropdown.append(option) optdata['label'] = html.escape(text) option.set_yaml(optdata, 'option') if dropdown is not None: node.append(dropdown) if 'randomized' in self.options: data['randomized'] = self.options.get('randomized', 1) if data['randomized'] > len(choices): source, line = self.state_machine.get_source_and_line( self.lineno) raise SphinxError( source + ": line " + str(line) + "\nThe option 'randomized' can not be greater than the number of answer choices!" ) if 'correct-count' in self.options: data['correct_count'] = self.options.get('correct-count', 0) if data['correct_count'] > correct_count or data[ 'correct_count'] > data['randomized']: source, line = self.state_machine.get_source_and_line( self.lineno) raise SphinxError( source + ": line " + str(line) + "\nThe option 'correct-count' can not be greater than " "the number of correct choices or the value of 'randomized'!" ) if 'preserve-questions-between-attempts' in self.options: data['resample_after_attempt'] = False env.aplus_random_question_exists = True if 'checkbox-feedback' in self.options: data['checkbox_feedback'] = True self.add_feedback(node, data, feedback) return [node]
def run(self): self.length = self.options.get('length', None) self.height = self.options.get('height', 1) self.position = u'place-on-own-line' if self.height > 1 or u'own-line' in self.options else u'place-inline' # Detect paragraphs: any number of plain content and correct answer including optional feedback plain_content = None config_content = [] env = self.state.document.settings.env if env.questionnaire_is_feedback: plain_content = self.content else: empty_lines = list(loc for loc,line in enumerate(self.content) if line == u"") if len(empty_lines) > 0: plain_content = self.content[:empty_lines[-1]] config_content = self.content[(empty_lines[-1] + 1):] else: config_content = self.content # Create question. env, node, data = self.create_question() self.add_instructions(node, data, plain_content) # Create input element. if self.height > 1: attrs = { u'rows': self.height, u'class': self.position, } if self.length: attrs[u'cols'] = self.length element = aplus_nodes.html(u'textarea', attrs) else: attrs = { u'type': u'text', u'class': self.position, } if self.length: attrs[u'size'] = self.length element = aplus_nodes.html(u'input', attrs) node.append(element) # Add configuration. if len(self.arguments) > 1: self._validate_compare_method(self.arguments[1]) data[u'compare_method'] = self.arguments[1] if config_content: fb = None if u'§' in config_content[0]: correct,fb = config_content[0].split(u'§', 1) else: correct = config_content[0] config_content = config_content[1:] if u'°=°' in correct: correct,model = correct.split(u'°=°', 1) data[u'model'] = model.strip().replace(u"°°°", u"\n") if fb: config_content[0] = correct.strip() + u" § " + fb.strip() data[u'correct'] = correct.strip().replace(u"°°°", u"\n") self.add_feedback(node, data, config_content) return [node]
def run(self): self.assert_has_content() # Detect paragraphs: any number of plain content, choices and optional feedback. empty_lines = list(loc for loc, line in enumerate(self.content) if line == u'') plain_content = None choices = [] feedback = [] if len(empty_lines) > 0: last = self.content[(empty_lines[-1] + 1):] def split_second_last(empty_lines): if len(empty_lines) > 1: return self.content[:empty_lines[-2]], self.content[( empty_lines[-2] + 1):empty_lines[-1]] else: return None, self.content[:empty_lines[-1]] # Backwards compatibility for skipping feedback paragraph. if len(last) == 1 and last[0].startswith( u'I hereby declare that no feedback '): plain_content, choices = split_second_last(empty_lines) elif all(u'§' in line for line in last): plain_content, choices = split_second_last(empty_lines) feedback = last else: plain_content = self.content[:empty_lines[-1]] choices = last else: choices = self.content # Create question. env, node, data = self.create_question() self.add_instructions(node, data, plain_content) data[u'options'] = (u'#!children', u'option') dropdown = None if self.grader_field_type() == 'dropdown': # The HTML select element has a different structure compared # to the input elements (radio buttons and checkboxes). dropdown = aplus_nodes.html( 'select', { 'name': 'field_{:d}'.format(env.question_count - 1), }) # Travel all answer options. for i, line in slicer(choices): # Split choice key off. key, content = line[0].split(u' ', 1) key = key.strip() line[0] = content.strip() # Trim the key. correct = False selected = False if key.startswith('+'): selected = True key = key[1:] if key.startswith(u'*'): correct = True key = key[1:] if key.endswith(u'.'): key = key[:-1] # Add YAML configuration data. optdata = { 'value': key, } if correct: optdata['correct'] = True if selected: optdata['selected'] = True # Create document elements. if dropdown is None: # One answer alternative as a radio button or a checkbox. choice = aplus_nodes.html('div', {'class': 'radio'}) label = aplus_nodes.html('label', {}) attrs = { 'type': self.input_type(), 'name': 'field_{:d}'.format(env.question_count - 1), 'value': key, } if selected: attrs['checked'] = 'checked' label.append(aplus_nodes.html('input', attrs)) choice.append(label) node.append(choice) text = aplus_nodes.html('span', {}) text.store_html('label') nested_parse_with_titles(self.state, line, text) label.append(text) optdata['label'] = ('#!html', 'label') choice.set_yaml(optdata, 'option') else: # Add option elements to the select. # Options may only contain plain text, not formatted HTML. attrs = { 'value': key, } if selected: attrs['selected'] = 'selected' option = aplus_nodes.html('option', attrs) text = line[0] option.append(nodes.Text(text)) dropdown.append(option) optdata['label'] = html.escape(text) option.set_yaml(optdata, 'option') if dropdown is not None: node.append(dropdown) self.add_feedback(node, data, feedback) return [node]
def run(self): self.assert_has_content() if len(self.arguments) == 0: return [nodes.container()] name = self.arguments[0] title_text = '' if 'title' in self.options: title_text = self.options['title'] node = nodes.container() title = nodes.container() content = nodes.container('\n'.join(self.content)) nav = nodes.container() links = nodes.container() # poi_nav node is later replaced by reference nodes which # need to be within a TextElement-node text = nodes.inline() poinav = poi_nav() # poinav id needs to be added so that we can identify the node later when processing poinav['ids'].append(name) container_class = 'poi-container' node['classes'].extend(['poi']) if 'class' in self.options: node['classes'].extend(self.options['class']) title['classes'].extend(['poi-title']) links['classes'].extend(['poi-links']) content['classes'].extend([container_class, 'poi-content', 'collapse']) if not 'hidden' in self.options: content['classes'].extend(['in']) nav['classes'].extend([container_class]) self.options['name'] = name self.add_name(node) content_name = name + '-content' content['ids'].append(content_name) icon = aplus_nodes.html(u'img', { u'src':u'../_static/poi.png', u'alt':u'Point of interest icon', u'class':u'poi-icon', }) hidelink = aplus_nodes.html(u'a', { u'href':u'#' + content_name, u'data-toggle':u'collapse'}) hidelink.append(icon) hidelink.append(nodes.Text(title_text)) title.append(hidelink) nav.append(title) text.append(poinav) links.append(text) nav.append(links) node.append(nav) node.append(content) self.state.nested_parse(self.content, self.content_offset, content) # poi_info needs to be stored to env to be able to construct the # refuris later env = self.state.document.settings.env poi_info = { 'docname': env.docname, } if 'previous' in self.options: poi_info['previous'] = self.options['previous'] if 'next' in self.options: poi_info['next'] = self.options['next'] if not hasattr(env, 'poi_all'): env.poi_all = {} env.poi_all[name] = poi_info return [node]
def run(self): self.assert_has_content() key, difficulty, points = self.extract_exercise_arguments() # Parse options. classes = [u'exercise'] is_feedback = False if 'chapter-feedback' in self.options: classes.append(u'chapter-feedback') is_feedback = True if 'weekly-feedback' in self.options: classes.append(u'weekly-feedback') is_feedback = True if 'appendix-feedback' in self.options: classes.append(u'appendix-feedback') is_feedback = True if 'course-feedback' in self.options: classes.append(u'course-feedback-questionnaire') is_feedback = True if 'feedback' in self.options: is_feedback = True if is_feedback: key = u'feedback' category = u'feedback' classes.append(u'feedback') else: category = u'questionnaire' if difficulty: classes.append(u'difficulty-' + difficulty) if 'category' in self.options: category = str(self.options.get('category')) env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override env.questionnaire_is_feedback = is_feedback env.question_count = 0 # Create document elements. node = aplus_nodes.html(u'div', { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', u'data-aplus-quiz': u'yes', }) form = aplus_nodes.html(u'form', { u'action': key, u'method': u'post', }) nested_parse_with_titles(self.state, self.content, form) submit = aplus_nodes.html(u'input', { u'type': u'submit', u'value': translations.get(env, u'submit'), u'class': u'btn btn-primary', }, skip_html=True) form.append(submit) node.append(form) # Write configuration file. data = { u'key': name, u'category': category, u'max_points': points, u'difficulty': difficulty or '', u'max_submissions': self.options.get('submissions', 0 if is_feedback else env.config.questionnaire_default_submissions), u'min_group_size': 1 if is_feedback else env.config.default_min_group_size, u'max_group_size': 1 if is_feedback else env.config.default_max_group_size, u'points_to_pass': self.options.get('points-to-pass', 0), u'feedback': is_feedback, u'view_type': u'access.types.stdsync.createForm', u'title|i18n': translations.opt('feedback') if is_feedback else translations.opt('exercise', postfix=u" {}".format(key)), u'fieldgroups': [{ u'title': '', u'fields': (u'#!children', None), }], } if not 'no-override' in self.options and category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if "pick_randomly" in self.options: pick_randomly = self.options.get('pick_randomly', 0) if pick_randomly < 1: raise SphinxError(u'Number of fields to sample randomly should greater than zero.') data[u'fieldgroups'][0]['pick_randomly'] = pick_randomly form.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.length = self.options.get('length', None) self.height = self.options.get('height', 1) self.position = u'place-on-own-line' if self.height > 1 or u'own-line' in self.options else u'place-inline' # Detect paragraphs: any number of plain content and correct answer including optional feedback plain_content = None config_content = [] env = self.state.document.settings.env if env.questionnaire_is_feedback: plain_content = self.content else: empty_lines = list(loc for loc, line in enumerate(self.content) if line == u"") if len(empty_lines) > 0: plain_content = self.content[:empty_lines[-1]] config_content = self.content[(empty_lines[-1] + 1):] else: config_content = self.content # Create question. env, node, data = self.create_question() self.add_instructions(node, data, plain_content) # Create input element. if self.height > 1: attrs = { u'rows': self.height, u'class': self.position, } if self.length: attrs[u'cols'] = self.length element = aplus_nodes.html(u'textarea', attrs) else: attrs = { u'type': u'text', u'class': self.position, } if self.length: attrs[u'size'] = self.length element = aplus_nodes.html(u'input', attrs) node.append(element) # Add configuration. if len(self.arguments) > 1: self._validate_compare_method(self.arguments[1]) data[u'compare_method'] = self.arguments[1] if config_content: fb = None if u'§' in config_content[0]: correct, fb = config_content[0].split(u'§', 1) else: correct = config_content[0] config_content = config_content[1:] if u'°=°' in correct: correct, model = correct.split(u'°=°', 1) data[u'model'] = model.strip().replace(u"°°°", u"\n") if fb: config_content[0] = correct.strip() + u" § " + fb.strip() data[u'correct'] = correct.strip().replace(u"°°°", u"\n") self.add_feedback(node, data, config_content) return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [u'exercise'] if 'class' in self.options: classes.extend(self.options['class']) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', u'data-aplus-active-element': u'out', u'data-inputs': u''+ self.options.get('inputs', ''), } if 'inputs' not in self.options: raise self.warning("The input list for output '{:s}' is empty.".format(key)) if 'type' in self.options: args['data-type'] = self.options['type'] else: args['data-type'] = 'text' if 'scale-size' in self.options: args['data-scale'] = '' if 'title' in self.options: args['data-title'] = self.options['title'] if 'width' in self.options: args['style'] = 'width:'+ self.options['width'] + ';' if 'height' in self.options: if 'style' not in args: args['style'] = 'height:'+ self.options['height'] + ';' else: args['style'] = args['style'] + 'height:'+ self.options['height'] + ';' if 'clear' in self.options: args['style'] = args['style'] + 'clear:'+ self.options['clear'] + ';' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) key_title = u"{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format(self.options['config'])) data = yaml_writer.read(path) config_title = data.get(u'title', None) else: data = { u'_external': True } if 'url' in self.options: data[u'url'] = ensure_unicode(self.options['url']) config_title = None config_title = self.options.get('title', config_title) category = u'submit' data.update({ u'key': name, u'title': env.config.submit_title.format( key_title=key_title, config_title=config_title ), u'category': u'active elements', u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.ae_default_submissions)), }) data.setdefault('status', self.options.get('status', 'unlisted')) if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): if len(self.arguments) > 0: key = self.arguments[0] else: raise SphinxError('Missing active element input id') env = self.state.document.settings.env name = "{}_{}".format(env.docname.replace('/', '_'), key) override = env.config.override classes = [] if 'class' in self.options: classes.extend(self.options['class']) # Add document nodes. args = { 'class': ' '.join(classes), 'data-aplus-active-element': 'in', 'id': '' + key, } if 'title' in self.options: args['data-title'] = self.options['title'] if 'default' in self.options: args['data-default'] = self.options['default'] if 'type' in self.options: args['data-type'] = self.options['type'] if 'width' in self.options: args['style'] = 'width:' + self.options['width'] + ';' if 'height' in self.options: if 'style' not in args: args['style'] = 'height:' + self.options['height'] + ';' else: args['style'] = args['style'] + 'height:' + self.options[ 'height'] + ';' if 'clear' in self.options: args['style'] = args['style'] + 'clear:' + self.options[ 'clear'] + ';' node = aplus_nodes.html('div', args) paragraph = aplus_nodes.html('p', {}) paragraph.append( nodes.Text(translations.get(env, 'active_element_placeholder'))) node.append(paragraph) # For clickable inputs the pre-generated html # needs to be added after the regular a+ exercise node if 'type' in self.options and self.options['type'] == 'clickable': if 'file' not in self.options: logger.warning( 'Clickable active element input "{}" missing template file.' .format(name), location=node) return [node] # Read given html file (from docutils.directives.misc.raw) source_dir = os.path.dirname( os.path.abspath(self.state.document.current_source)) path = os.path.join(env.app.srcdir, self.options['file']) path = utils.relative_path(None, path) try: raw_file = io.FileInput( source_path=path, encoding=self.state.document.settings.input_encoding, error_handler=self.state.document.settings. input_encoding_error_handler) except IOError as error: logger.error('Problem with "%s" directive:\n%s.' % (self.name, ErrorString(error)), location=node) return [] try: text = raw_file.read() except UnicodeError as error: logger.error('Problem with "%s" directive:\n%s.' % (self.name, ErrorString(error)), location=node) return [] # Generate raw node rawnode = nodes.raw('', text, **{'format': 'html'}) wrapnode = aplus_nodes.html('div', { 'id': '' + key + '-wrap', 'class': 'clickable-ae-wrapper' }) wrapnode.append(node) wrapnode.append(rawnode) return [wrapnode] return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [u'exercise'] if 'class' in self.options: classes.extend(self.options['class']) if difficulty: classes.append(u'difficulty-' + difficulty) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', } if 'quiz' in self.options: args[u'data-aplus-quiz'] = u'yes' if 'ajax' in self.options: args[u'data-aplus-ajax'] = u'yes' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) key_title = u"{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format(self.options['config'])) data = yaml_writer.read(path) config_title = data.get(u'title', None) else: data = { u'_external': True } if 'url' in self.options: data[u'url'] = ensure_unicode(self.options['url']) if 'lti' in self.options: data.update({ u'lti': ensure_unicode(self.options['lti']), u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')), u'lti_resource_link_id': ensure_unicode(self.options.get('lti_resource_link_id', u'')), }) config_title = None config_title = self.options.get('title', config_title) if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options: data[u'radar_info'] = { u'tokenizer': self.options.get("radar_tokenizer"), u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"), } category = u'submit' data.update({ u'key': name, u'title': env.config.submit_title.format( key_title=key_title, config_title=config_title ), u'category': u'submit', u'scale_points': points, u'difficulty': difficulty or '', u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.program_default_submissions)), u'min_group_size': data.get('min_group_size', env.config.default_min_group_size), u'max_group_size': data.get('max_group_size', env.config.default_max_group_size), u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)), }) if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if 'category' in self.options: data['category'] = str(self.options['category']) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() key, difficulty, points = self.extract_exercise_arguments() # Parse options. classes = [u'exercise'] is_feedback = False if 'chapter-feedback' in self.options: classes.append(u'chapter-feedback') is_feedback = True if 'weekly-feedback' in self.options: classes.append(u'weekly-feedback') is_feedback = True if 'appendix-feedback' in self.options: classes.append(u'appendix-feedback') is_feedback = True if 'course-feedback' in self.options: classes.append(u'course-feedback-questionnaire') is_feedback = True if 'feedback' in self.options: is_feedback = True if is_feedback: key = u'feedback' category = u'feedback' classes.append(u'feedback') else: category = u'questionnaire' if difficulty: classes.append(u'difficulty-' + difficulty) env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override env.questionnaire_is_feedback = is_feedback env.question_count = 0 # Create document elements. node = aplus_nodes.html( u'div', { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', u'data-aplus-quiz': u'yes', }) form = aplus_nodes.html(u'form', { u'action': key, u'method': u'post', }) nested_parse_with_titles(self.state, self.content, form) submit = aplus_nodes.html(u'input', { u'type': u'submit', u'value': translations.get(env, u'submit'), u'class': u'btn btn-primary', }, skip_html=True) form.append(submit) node.append(form) # Write configuration file. data = { u'key': name, u'category': category, u'max_points': points, u'difficulty': difficulty or '', u'max_submissions': self.options.get( 'submissions', 0 if is_feedback else env.config.questionnaire_default_submissions), u'min_group_size': 1 if is_feedback else env.config.default_min_group_size, u'max_group_size': 1 if is_feedback else env.config.default_max_group_size, u'points_to_pass': self.options.get('points-to-pass', 0), u'feedback': is_feedback, u'view_type': u'access.types.stdsync.createForm', u'title|i18n': translations.opt('feedback') if is_feedback else translations.opt( 'exercise', postfix=u" {}".format(key)), u'fieldgroups': [{ u'title': '', u'fields': (u'#!children', None), }], } if not 'no-override' in self.options and category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if "pick_randomly" in self.options: pick_randomly = self.options.get('pick_randomly', 0) if pick_randomly < 1: raise SphinxError( u'Number of fields to sample randomly should greater than zero.' ) data[u'fieldgroups'][0]['pick_randomly'] = pick_randomly form.write_yaml(env, name, data, 'exercise') return [node]
def run(self): if len(self.arguments) > 0: key = self.arguments[0] else: raise SphinxError('Missing active element input id') env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [] if 'class' in self.options: classes.extend(self.options['class']) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-active-element': u'in', u'id': u''+ key, } if 'title' in self.options: args['data-title'] = self.options['title'] if 'default' in self.options: args['data-default'] = self.options['default'] if 'type' in self.options: args['data-type'] = self.options['type'] if 'width' in self.options: args['style'] = 'width:'+ self.options['width'] + ';' if 'height' in self.options: if 'style' not in args: args['style'] = 'height:'+ self.options['height'] + ';' else: args['style'] = args['style'] + 'height:'+ self.options['height'] + ';' if 'clear' in self.options: args['style'] = args['style'] + 'clear:'+ self.options['clear'] + ';' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append(nodes.Text(translations.get(env, 'active_element_placeholder'))) node.append(paragraph) # For clickable inputs the pre-generated html # needs to be added after the regular a+ exercise node if 'type' in self.options and self.options['type'] == 'clickable': if 'file' not in self.options: logger.warning('Clickable active element input "{}" missing template file.' .format(name), location=node) return [node] # Read given html file (from docutils.directives.misc.raw) source_dir = os.path.dirname( os.path.abspath(self.state.document.current_source)) path = os.path.join(env.app.srcdir, self.options['file']) path = utils.relative_path(None, path) try: raw_file = io.FileInput(source_path=path, encoding=self.state.document.settings.input_encoding, error_handler=self.state.document.settings.input_encoding_error_handler) except IOError as error: logger.error(u'Problem with "%s" directive:\n%s.' % (self.name, ErrorString(error)), location=node) return [] try: text = raw_file.read() except UnicodeError as error: logger.error(u'Problem with "%s" directive:\n%s.' % (self.name, ErrorString(error)), location=node) return [] # Generate raw node rawnode = nodes.raw('', text, **{'format':'html'}) wrapnode = aplus_nodes.html(u'div', {u'id': u''+ key + '-wrap',u'class': u'clickable-ae-wrapper'}) wrapnode.append(node) wrapnode.append(rawnode) return [wrapnode] return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [u'exercise'] if 'class' in self.options: classes.extend(self.options['class']) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', u'data-aplus-active-element': u'out', u'data-inputs': u''+ self.options.get('inputs', ''), } if 'inputs' not in self.options: raise self.warning("The input list for output '{:s}' is empty.".format(key)) if 'type' in self.options: args['data-type'] = self.options['type'] else: args['data-type'] = 'text' if 'scale-size' in self.options: args['data-scale'] = '' if 'title' in self.options: args['data-title'] = self.options['title'] if 'width' in self.options: args['style'] = 'width:'+ self.options['width'] + ';' if 'height' in self.options: if 'style' not in args: args['style'] = 'height:'+ self.options['height'] + ';' else: args['style'] = args['style'] + 'height:'+ self.options['height'] + ';' if 'clear' in self.options: args['style'] = args['style'] + 'clear:'+ self.options['clear'] + ';' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) key_title = u"{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format(self.options['config'])) data = yaml_writer.read(path) config_title = data.get(u'title', None) else: data = { u'_external': True } if 'url' in self.options: data[u'url'] = ensure_unicode(self.options['url']) config_title = None config_title = self.options.get('title', config_title) category = u'submit' data.update({ u'key': name, u'title': env.config.submit_title.format( key_title=key_title, config_title=config_title ), u'category': u'active elements', u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.ae_default_submissions)), }) if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [u'exercise'] if 'class' in self.options: classes.extend(self.options['class']) if difficulty: classes.append(u'difficulty-' + difficulty) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', } if 'quiz' in self.options: args[u'data-aplus-quiz'] = u'yes' if 'ajax' in self.options: args[u'data-aplus-ajax'] = u'yes' node = aplus_nodes.html(u'div', args) paragraph = aplus_nodes.html(u'p', {}) paragraph.append( nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) key_title = u"{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format( self.options['config'])) data = yaml_writer.read(path) config_title = data.get(u'title', None) else: data = {u'_external': True} if 'url' in self.options: data[u'url'] = ensure_unicode(self.options['url']) if 'lti' in self.options: data.update({ u'lti': ensure_unicode(self.options['lti']), u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')), u'lti_resource_link_id': ensure_unicode( self.options.get('lti_resource_link_id', u'')), }) config_title = None config_title = self.options.get('title', config_title) if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options: data[u'radar_info'] = { u'tokenizer': self.options.get("radar_tokenizer"), u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"), } category = u'submit' data.update({ u'key': name, u'title': env.config.submit_title.format(key_title=key_title, config_title=config_title), u'category': u'submit', u'scale_points': points, u'difficulty': difficulty or '', u'max_submissions': self.options.get( 'submissions', data.get('max_submissions', env.config.program_default_submissions)), u'min_group_size': data.get('min_group_size', env.config.default_min_group_size), u'max_group_size': data.get('max_group_size', env.config.default_max_group_size), u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)), }) if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if 'category' in self.options: data['category'] = str(self.options['category']) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() # Detect paragraphs: any number of plain content, choices and optional feedback. empty_lines = list(loc for loc,line in enumerate(self.content) if line == u'') plain_content = None choices = [] feedback = [] if len(empty_lines) > 0: last = self.content[(empty_lines[-1] + 1):] def split_second_last(empty_lines): if len(empty_lines) > 1: return self.content[:empty_lines[-2]], self.content[(empty_lines[-2] + 1):empty_lines[-1]] else: return None, self.content[:empty_lines[-1]] # Backwards compatibility for skipping feedback paragraph. if len(last) == 1 and last[0].startswith(u'I hereby declare that no feedback '): plain_content, choices = split_second_last(empty_lines) elif all(u'§' in line for line in last): plain_content, choices = split_second_last(empty_lines) feedback = last else: plain_content = self.content[:empty_lines[-1]] choices = last else: choices = self.content # Create question. env, node, data = self.create_question() self.add_instructions(node, data, plain_content) data[u'options'] = (u'#!children', u'option') # Travel all answer options. for i,line in slicer(choices): # Split choice key off. key,content = line[0].split(u' ', 1) key = key.strip() line[0] = content.strip() # Trim the key. correct = False if key.startswith(u'*'): correct = True key = key[1:] if key.endswith(u'.'): key = key[:-1] # Create document elements. choice = aplus_nodes.html(u'div', {u'class':u'radio'}) label = aplus_nodes.html(u'label', {}) label.append(aplus_nodes.html(u'input', { u'type': self.input_type(), u'name': u'field_{:d}'.format(env.question_count - 1), u'value': key, })) choice.append(label) node.append(choice) text = aplus_nodes.html(u'span', {}) text.store_html(u'label') nested_parse_with_titles(self.state, line, text) label.append(text) # Add configuration data. optdata = { u'value': key, u'label': (u'#!html', u'label'), } if correct: optdata[u'correct'] = True choice.set_yaml(optdata, u'option') self.add_feedback(node, data, feedback) return [node]
def run(self): key, difficulty, points = self.extract_exercise_arguments() env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override classes = [u'exercise'] if 'class' in self.options: classes.extend(self.options['class']) if difficulty: classes.append(u'difficulty-' + difficulty) # Add document nodes. args = { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', } if 'quiz' in self.options: args[u'data-aplus-quiz'] = u'yes' if 'ajax' in self.options: args[u'data-aplus-ajax'] = u'yes' node = aplus_nodes.html(u'div', args) key_title = u"{} {}".format(translations.get(env, 'exercise'), key) # Load or create exercise configuration. if 'config' in self.options: path = os.path.join(env.app.srcdir, self.options['config']) if not os.path.exists(path): raise SphinxError('Missing config path {}'.format(self.options['config'])) data = yaml_writer.read(path) config_title = data.get(u'title', None) else: data = { u'_external': True } if 'url' in self.options: data[u'url'] = ensure_unicode(self.options['url']) if 'lti' in self.options: data.update({ u'lti': ensure_unicode(self.options['lti']), u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')), u'lti_resource_link_id': ensure_unicode(self.options.get('lti_resource_link_id', u'')), }) if 'lti_aplus_get_and_post' in self.options: data.update({u'lti_aplus_get_and_post': True}) if 'lti_open_in_iframe' in self.options: data.update({u'lti_open_in_iframe': True}) config_title = None config_title = self.options.get('title', config_title) if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options: data[u'radar_info'] = { u'tokenizer': self.options.get("radar_tokenizer"), u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"), } category = u'submit' data.update({ u'key': name, u'title': env.config.submit_title.format( key_title=key_title, config_title=config_title ), u'category': u'submit', u'scale_points': points, u'difficulty': difficulty or '', u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.program_default_submissions)), u'min_group_size': data.get('min_group_size', env.config.default_min_group_size), u'max_group_size': data.get('max_group_size', env.config.default_max_group_size), u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)), }) self.set_assistant_permissions(data) if self.content: self.assert_has_content() nested_parse_with_titles(self.state, self.content, node) # Sphinx can not compile the nested RST into HTML at this stage, hence # the HTML instructions defined in this directive body are added to # the exercise YAML file only at the end of the build. Sphinx calls # the visit functions of the nodes in the last writing phase. # The instructions are added to the YAML file in the depart_html # function in aplus_nodes.py. else: paragraph = aplus_nodes.html(u'p', {}) paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder'))) node.append(paragraph) data.setdefault('status', self.options.get('status', 'unlisted')) if category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if 'category' in self.options: data['category'] = str(self.options['category']) node.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() key, difficulty, points = self.extract_exercise_arguments() # Parse options. classes = [u'exercise'] is_feedback = False if 'chapter-feedback' in self.options: classes.append(u'chapter-feedback') is_feedback = True if 'weekly-feedback' in self.options: classes.append(u'weekly-feedback') is_feedback = True if 'appendix-feedback' in self.options: classes.append(u'appendix-feedback') is_feedback = True if 'course-feedback' in self.options: classes.append(u'course-feedback-questionnaire') is_feedback = True if 'feedback' in self.options: is_feedback = True if is_feedback: key = u'feedback' category = u'feedback' classes.append(u'feedback') else: category = u'questionnaire' if difficulty: classes.append(u'difficulty-' + difficulty) if 'category' in self.options: category = str(self.options.get('category')) env = self.state.document.settings.env name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key) override = env.config.override env.questionnaire_is_feedback = is_feedback env.question_count = 0 env.aplus_single_question_points = None env.aplus_quiz_total_points = 0 env.aplus_pick_randomly_quiz = 'pick_randomly' in self.options # Create document elements. node = aplus_nodes.html( u'div', { u'class': u' '.join(classes), u'data-aplus-exercise': u'yes', u'data-aplus-quiz': u'yes', }) form = aplus_nodes.html(u'form', { u'action': key, u'method': u'post', }) nested_parse_with_titles(self.state, self.content, form) submit = aplus_nodes.html(u'input', { u'type': u'submit', u'value': translations.get(env, u'submit'), u'class': u'btn btn-primary', }, skip_html=True) form.append(submit) node.append(form) # Write configuration file. data = { u'key': name, u'category': category, u'difficulty': difficulty or '', u'max_submissions': self.options.get( 'submissions', 0 if is_feedback else env.config.questionnaire_default_submissions), u'min_group_size': 1 if is_feedback else env.config.default_min_group_size, u'max_group_size': 1 if is_feedback else env.config.default_max_group_size, u'points_to_pass': self.options.get('points-to-pass', 0), u'feedback': is_feedback, u'view_type': u'access.types.stdsync.createForm', u'status': self.options.get('status', 'unlisted'), u'fieldgroups': [{ u'title': '', u'fields': (u'#!children', None), }], } self.set_assistant_permissions(data) points_set_in_arguments = len( self.arguments) == 2 and difficulty != self.arguments[1] if 'pick_randomly' in self.options: calculated_max_points = (self.options.get('pick_randomly') * env.aplus_single_question_points if env.aplus_single_question_points is not None else 0) else: calculated_max_points = env.aplus_quiz_total_points if calculated_max_points == 0 and is_feedback: data['max_points'] = points else: if points_set_in_arguments and calculated_max_points != points: source, line = self.state_machine.get_source_and_line( self.lineno) raise SphinxError( source + ": line " + str(line) + "\nThe points of the questions in the questionnaire must add up to the total points of the questionnaire!" ) data['max_points'] = calculated_max_points if 'title' in self.options: data['title'] = self.options.get('title') else: data[u'title|i18n'] = translations.opt( 'feedback') if is_feedback else translations.opt( 'exercise', postfix=u" {}".format(key)) if not 'no-override' in self.options and category in override: data.update(override[category]) if 'url' in data: data['url'] = data['url'].format(key=name) if "pick_randomly" in self.options: pick_randomly = self.options.get('pick_randomly', 0) if pick_randomly < 1: raise SphinxError( u'Number of fields to sample randomly should greater than zero.' ) data[u'fieldgroups'][0]['pick_randomly'] = pick_randomly form.write_yaml(env, name, data, 'exercise') return [node]
def run(self): self.assert_has_content() if len(self.arguments) == 0: return [nodes.container()] name = self.arguments[0] title_text = '' if 'title' in self.options: title_text = self.options['title'] node = nodes.container() title = nodes.container() content = nodes.container('\n'.join(self.content)) nav = nodes.container() links = nodes.container() # poi_nav node is later replaced by reference nodes which # need to be within a TextElement-node text = nodes.inline() poinav = poi_nav() # poinav id needs to be added so that we can identify the node later when processing poinav['ids'].append(name) container_class = 'poi-container' node['classes'].extend(['poi']) if 'class' in self.options: node['classes'].extend(self.options['class']) title['classes'].extend(['poi-title']) links['classes'].extend(['poi-links']) content['classes'].extend([container_class, 'poi-content', 'collapse']) if not 'hidden' in self.options: content['classes'].extend(['in']) nav['classes'].extend([container_class]) self.options['name'] = name self.add_name(node) content_name = name + '-content' content['ids'].append(content_name) icon = aplus_nodes.html( u'img', { u'src': u'../_static/poi.png', u'alt': u'Point of interest icon', u'class': u'poi-icon', }) hidelink = aplus_nodes.html(u'a', { u'href': u'#' + content_name, u'data-toggle': u'collapse' }) hidelink.append(icon) hidelink.append(nodes.Text(title_text)) title.append(hidelink) nav.append(title) text.append(poinav) links.append(text) nav.append(links) node.append(nav) node.append(content) self.state.nested_parse(self.content, self.content_offset, content) # poi_info needs to be stored to env to be able to construct the # refuris later env = self.state.document.settings.env poi_info = { 'docname': env.docname, } if 'previous' in self.options: poi_info['previous'] = self.options['previous'] if 'next' in self.options: poi_info['next'] = self.options['next'] if not hasattr(env, 'poi_all'): env.poi_all = {} env.poi_all[name] = poi_info return [node]
def run(self): self.assert_has_content() env = self.state.document.settings.env if not hasattr(env, 'poi_all'): env.poi_all = {} if len(self.arguments) == 0: return [nodes.container()] if 'title' in self.options: if 'id' in self.options: raise SphinxError(u'Point of interest options can\'t contain both "title" and "id"; one of them should be provided as an argument instead') name = self.arguments[0] title_text = self.options['title'] else: name = self.options.get('id') if not name: choices = string.ascii_lowercase + string.digits while True: name = ''.join(random.choice(choices) for i in range(6)) if name not in env.poi_all: break title_text = self.arguments[0] container_class = 'poi-container' content_name = name + '-content' node = nodes.container() title = nodes.container() # add an extra div to force content to desired height hcontainer_opts = { u'style':'height:'+self.options.get('height', '')+';', u'class': container_class +' poi-content row', } if 'bgimg' in self.options: static_host = os.environ.get('STATIC_CONTENT_HOST', None) if not static_host: logger.warning('Environment variable STATIC_CONTENT_HOST must be set to be able to use point of interest background', location=node) else: docname = env.docname imgname = self.options['bgimg'].split('/')[-1] imgpath = ('/').join(docname.split('/')[0:-1]) + '/' + self.options['bgimg'] urlstring = 'background-image:url(' + static_host + '/_images/' + imgname + ');' hcontainer_opts['style'] = hcontainer_opts['style'] + urlstring # Add background image to env and builder so that it will # be correctly copied to static _images build directory env.images[imgpath] = ({docname}, imgname) env.app.builder.images[imgpath] = imgname hcontainer = aplus_nodes.html(u'div', hcontainer_opts) collapsible = nodes.container() contentnodes = [] colseparator = '::newcol' colcontent = '' colcontent_l = [] colwidths = None prevl = 0 l = 0 if 'columns' in self.options: cols = list(map(int,self.options['columns'].split())) allcols = sum(cols) colwidths = [x / allcols for x in cols] for batch in self.content: l += 1 if batch == colseparator: cn = len(contentnodes) cwidth = None if colwidths: cwidth = floor(colwidths[cn] * 12) cnode = nodes.container(colcontent) hcontainer.append(cnode) contentnodes.append((self.content[prevl:l-1], self.content_offset + prevl, cnode, cwidth)) colcontent = '' prevl = l else: colcontent_l.append(batch) colcontent += batch colcontent += '\n' # add last column cnode = nodes.container(colcontent) hcontainer.append(cnode) cwidth = None if colwidths: cwidth = floor(colwidths[-1] * 12) contentnodes.append((self.content[prevl:], self.content_offset + prevl, cnode, cwidth)) nav = nodes.container() links = nodes.container() # poi_nav node is later replaced by reference nodes which # need to be within a TextElement-node text = nodes.inline() poinav = poi_nav() icon = aplus_nodes.html(u'img', { u'src':u'../_static/poi.png', u'alt':u'Point of interest icon', u'class':u'poi-icon', }) hidelink = aplus_nodes.html(u'a', { u'href':u'#' + content_name, u'data-toggle':u'collapse'}) hidelink.append(icon) hidelink.append(nodes.Text(title_text)) title.append(hidelink) nav.append(title) text.append(poinav) links.append(text) nav.append(links) node.append(nav) node.append(collapsible) collapsible.append(hcontainer) numcols = len(contentnodes) for cont, offset, cnode, cwidth in contentnodes: # Bootstrap uses 12 unit grid if not cwidth: cwidth = int(12/numcols) cnode['classes'] = ['col-sm-' + str(cwidth)] self.state.nested_parse(cont, offset, cnode) # poinav id needs to be added so that we can identify the node later when processing poinav['ids'].append(name) self.options['name'] = name self.add_name(node) collapsible['ids'].append(content_name) collapsible['classes'].extend([container_class, 'collapse']) node['classes'].extend(['poi']) if 'class' in self.options: node['classes'].extend(self.options['class']) title['classes'].extend(['poi-title']) links['classes'].extend(['poi-links']) if not 'hidden' in self.options: collapsible['classes'].extend(['in']) nav['classes'].extend([container_class]) # poi_info needs to be stored to env to be able to construct the # refuris later poi_info = { 'docname': env.docname, } if 'previous' in self.options: poi_info['previous'] = self.options['previous'] if 'next' in self.options: poi_info['next'] = self.options['next'] env.poi_all[name] = poi_info return [node]