Ejemplo n.º 1
0
def answer_randomly(task,
                    overwrite=False,
                    halt_impute=True,
                    skip_impute=False,
                    quiet=False):
    def log(item):
        if not quiet:
            print(item)

    current_answers = [x for x in task.get_current_answer_records()]

    # sooo... sometimes we might have accidentally created a project with no admin. Use the org admin instead.
    dummy_user = task.project.organization.get_organization_project(
    ).get_admins()[0]

    # we want to communicate back to the caller whether this was fully skipped or not
    did_anything = False

    for question in task.module.questions.order_by('definition_order'):
        type = question.spec['type']

        if type == 'raw':
            print("'raw' question type is out-of-scope, skipping")
            continue

        if (halt_impute or skip_impute) and 'impute' in question.spec:
            log("'impute' handling not yet implemented, skipping " +
                question.key)
            if halt_impute:
                break
            continue

        if not overwrite:
            has_answer = len([
                x for x in current_answers if x[1] and x[0].key == question.key
            ]) > 0
            if has_answer:
                log("Already answered " + question.key)
                continue

        answer = None
        if type == 'yesno':
            answer = sample(['yes', 'no'], 1)[0]
        elif type == 'text':
            answer = get_random_sentence()
        elif type == 'longtext':
            answer = get_random_paragraph()
        elif type == 'choice':
            answer = sample(question.spec['choices'], 1)[0]['key']
        elif type == 'multiple-choice':
            choices = question.spec['choices']
            amount = randint(question.spec['min'], len(choices))
            answer = [x['key'] for x in sample(choices, amount)]
        elif type == 'datagrid':
            choices = question.spec['fields']
            amount = randint(question.spec['min'], len(fields))
            answer = [x['key'] for x in sample(fields, amount)]
        elif type == 'module' and 'module-id' in question.spec:
            subtask = task.get_or_create_subtask(dummy_user,
                                                 question,
                                                 create=True)
            log("doing subtask")
            did_anything = True
            continue

        if not answer and not (type == 'interstitial' or type == 'raw'):
            print("Cannot answer question of type '" + type + "'")
            continue

        did_anything = True

        log(str((question.key, type, answer)))
        taskans, isnew = TaskAnswer.objects.get_or_create(task=task,
                                                          question=question)

        from guidedmodules.answer_validation import validator
        try:
            value = validator.validate(question, answer)
        except ValueError as e:
            print("Answering {}: {}...".format(question.key, e))
            #return False
            break
        except Error as e:
            print("------\nUnknown error occurred, debug info follows.\n")
            print(
                "Next line is: 'str((question.key, type, answer, question.spec))'"
            )
            print(str((question.key, type, answer, question.spec)))
            print("------")
            raise e

        # Save the value.
        if taskans.save_answer(value, [], None, dummy_user, "api"):
            log("Answered {} with {}...".format(question.key, answer))
        else:
            log("No change?")
            break

        return did_anything
Ejemplo n.º 2
0
def project_api(request, org_slug, project_id):
    from collections import OrderedDict

    # Get user from API key.
    api_key = request.META.get("HTTP_AUTHORIZATION", "").strip()
    if len(
            api_key
    ) < 32:  # prevent null values from matching against users without api keys
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "An API key was not present in the Authorization header.")
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    from django.db.models import Q
    from .models import User, Project

    try:
        user = User.objects.get(
            Q(api_key_rw=api_key) | Q(api_key_ro=api_key)
            | Q(api_key_wo=api_key))
    except User.DoesNotExist:
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "A valid API key was not present in the Authorization header.")
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    # Get project and check authorization.
    organization = get_object_or_404(Organization, subdomain=org_slug)
    project = get_object_or_404(Project,
                                id=project_id,
                                organization=organization)
    if not project.has_read_priv(user):
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "The user associated with the API key does not have read perission on the project."
             )
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    if request.method == "GET":
        # Export data.
        value = project.export_json(include_file_content=False,
                                    include_metadata=False)

        # Return the value as JSON.
        return JsonResponse(value, json_dumps_params={"indent": 2})

    elif request.method == "POST":
        # Update the value.

        # Check that the API key has write access.
        if not project.root_task.has_write_priv(user):
            return JsonResponse(OrderedDict([
                ("status", "error"),
                ("error",
                 "The user associated with the API key does not have write perission on the project."
                 )
            ]),
                                json_dumps_params={"indent": 2},
                                status=403)
        if api_key not in (user.api_key_rw, user.api_key_wo):
            return JsonResponse(OrderedDict([
                ("status", "error"),
                ("error",
                 "The API key does not have write perission on the project.")
            ]),
                                json_dumps_params={"indent": 2},
                                status=403)

        # Parse the new project body.
        if request.META.get("CONTENT_TYPE") == "application/json":
            # A JSON object is given in the import/export format. Parse it.
            import json
            try:
                value = json.loads(request.body.decode("utf-8"))
            except json.decoder.JSONDecodeError as e:
                return JsonResponse(OrderedDict([
                    ("status", "error"),
                    ("error", "Invalid JSON in request body. " + str(e))
                ]),
                                    json_dumps_params={"indent": 2},
                                    status=400)

            # Update.
            log = []
            ok = project.import_json(value, user, "api",
                                     lambda msg: log.append(msg))

        else:
            # Form fields are given in POST and FILES. Process each item...

            from guidedmodules.models import TaskAnswer
            from guidedmodules.answer_validation import question_input_parser, validator
            import django.core.files.uploadedfile

            # For each item...
            log = []
            ok = True
            for key, v in list(request.POST.lists()) + list(
                    request.FILES.items()):
                try:
                    # The item is a dotted path of project + question IDs to the
                    # question to update. Follow the path to find the Task and ModuleQuestion
                    # to update. Start with the project root task, then for each item in the path...
                    task = project.root_task
                    question = None
                    if not key.startswith("project."):
                        raise ValueError("Invalid question ID: " + key)
                    for i, pathitem in enumerate(key.split(".")[1:]):
                        # If this is not the first item, then in the last iteration we
                        # were left with a Task and a ModuleQuestion in that task. Since
                        # we've continued with another dot and path part, the last ModuleQuestion
                        # must have been a module-type question. Move to its *answer*,
                        # which is another Task.
                        if question is not None:
                            if question.spec["type"] != "module":
                                raise ValueError("Invalid question ID: " + key)
                            try:
                                task = task.get_or_create_subtask(
                                    user, question)
                            except ValueError:
                                # Raised if the question is not answered and the question uses
                                # a protocol for selecting compliance apps rather than specifying
                                # a concrete Module to use for answers. In this case, we can't
                                # start a Task implicitly.
                                raise ValueError(
                                    "Invalid question ID '{}': {} has not been answered yet by a compliance app so its data fields cannot be set."
                                    .format(key,
                                            ".".join(key.split(".")[:i + 1])))

                        # Get the question this corresponds to within the task
                        question = task.module.questions.filter(
                            key=pathitem).first()
                        if not question:
                            raise ValueError("Invalid question ID: " + key)

                    # Parse and validate the answer.
                    v = question_input_parser.parse(question, v)
                    v = validator.validate(question, v)

                    # Save.
                    v_file = None
                    if question.spec["type"] == "file":
                        v_file = v
                        v = None
                    ta, _ = TaskAnswer.objects.get_or_create(task=task,
                                                             question=question)
                    saved = ta.save_answer(v, [], v_file, user, "api")
                    if saved:
                        log.append(key + " updated")
                    else:
                        log.append(key + " unchanged")

                except ValueError as e:
                    log.append(key + ": " + str(e))
                    ok = False

        return JsonResponse(OrderedDict([
            ("status", "ok" if ok else "error"),
            ("details", log),
        ]),
                            json_dumps_params={"indent": 2})
Ejemplo n.º 3
0
    def set_answer(self, task, question, answer, update_answer_func, basedir,
                   options):
        # Set the answer to the question for the given task.

        if question.spec[
                "type"] == "module" and question.answer_type_module is not None:
            # If there is no answer provided, normally we leave
            # the answer blank. However, for module-type questions
            # with a module type (not a protocol), we should at least
            # *start* the sub-task with the module answer type, even if we
            # don't answer any of its questions, because it may be
            # a question-less module that only provides output documents.
            # And because we use get_or_create_subtask, it doesn't matter
            # if this question was imputed.
            sub_task = task.get_or_create_subtask(self.dummy_user, question)
            # self.log("OK", "Answered {} with {}.".format(question.key, sub_task))
            if answer is None and options["add_blank_answers"]:
                answer = update_answer_func({"questions": []})
            if answer is not None:
                self.set_answers(sub_task, answer.setdefault("questions", []),
                                 basedir, options)
            return

        # Check if the question is answerable by computing the imputed
        # answers to this task so far. If the question is not answerable,
        # issue a warning if the user tried to answer it in the input
        # file, and then move on.
        if question not in task.get_answers().with_extended_info().can_answer:
            if answer is not None:
                self.log(
                    "WARN",
                    "The answer for {} in {} will be ignored because this question's answer is imputed from other values."
                    .format(question.key, self.str_task(task)))
            return

        # If the question isn't answered...
        if answer is None:
            # If --add-blank-answers is specified on the command line,
            # add a blank answer record to the YAML for any unanswered
            # question so the user has an easy spot to provide an answer.
            # Does not apply to interstitial questions which have no
            # meaningful answers and module/module-set questions which
            # we either handled above or can't handle this way.
            if options["add_blank_answers"] \
                  and question.spec["type"] not in ("module", "module-set", "raw"):
                update_answer_func({"answer": None})
                return

            # If --startapps is given on the command-line, then choose an
            # app automatically for module-type questions with protocols
            # and keep going to save the choice below.
            elif options["startapps"] and question.spec["type"] == "module" \
                  and question.answer_type_module is None and question.spec.get("protocol"):
                # Look for an app that can be used to answer this question.
                from siteapp.views import app_satifies_interface
                for app in options["startapps"]:
                    if app_satifies_interface(app["catalog_info"],
                                              question.spec.get("protocol")):
                        # We found one. Set the answer to be the path to the app,
                        # adjusted to be relative to the location of the assemble
                        # YAML file.
                        self.log(
                            "INFO",
                            "Choosing app {} for {} (from --startapps).".
                            format(app["app"], question.key))
                        answer = update_answer_func(
                            {"app": os.path.relpath(app["app"], basedir)})
                        break

                if answer is None:
                    # If no app was available, abort.
                    self.log(
                        "WARN",
                        "No app was available to answer {} in {}.".format(
                            question.key, self.str_task(task)))
                    return

            else:
                # If the question isn't answered, and we aren't initializing it using
                # one of the above options, warn and leave it alone.
                self.log(
                    "WARN", "No answer was provided for {} in {}.".format(
                        question.key, self.str_task(task)))
                return

        # Set an answer to the question.

        # Get the TaskAnswer record, which has the save_answer function.
        taskans, isnew = TaskAnswer.objects.get_or_create(task=task,
                                                          question=question)

        if question.spec["type"] in ("module", "module-set"):
            # A module-type question with a protocol (if there was no protocol, it
            # was handled above) or a module-set question (with or without a protocol).

            if question.spec["type"] == "module":
                answers = [answer]
            else:
                answers = answer.setdefault("answers", [])

            # Start the app(s).
            subtasks = []
            for i, answer in enumerate(answers):
                if question.answer_type_module is not None:
                    # Start the sub-task.
                    subtasks.append(
                        task.get_or_create_subtask(self.dummy_user, question))
                else:
                    # Start the app. The app to start is specified in the 'app' key.
                    if not isinstance(answer, dict):
                        self.log(
                            "ERROR",
                            "Invalid data type of answer for {} element {} in {}, got {}."
                            .format(question.key, i, self.str_task(task),
                                    repr(answer)))
                        return

                    project = self.start_app(answer.get("app"), basedir)

                    if project is None:
                        return  # error

                    # Validate that the protocols match.
                    unimplemented_protocols = set(
                        question.spec.get("protocol", [])) - set(
                            project.root_task.module.spec.get("protocol", []))
                    if unimplemented_protocols:
                        # There are unimplemented protocols.
                        self.log(
                            "ERROR",
                            "{} doesn't implement the protocol {} required to answer {}."
                            .format(project.root_task.module.app,
                                    ", ".join(sorted(unimplemented_protocols)),
                                    question.key))
                        return

                    # Keep the root Task to be an answer to the question.
                    subtasks.append(project.root_task)

            # Save the answer.
            if taskans.save_answer(None, subtasks, None, self.dummy_user,
                                   "api"):
                self.log(
                    "OK", "Answered {} with {}.".format(
                        question.key,
                        ", ".join([self.str_task(t) for t in subtasks])))

                # Set answers of sub-task.
                for subtask, subtask_answers in zip(subtasks, answers):
                    self.set_answers(
                        subtask, subtask_answers.setdefault("questions", []),
                        basedir, options)

                return

        else:
            # This is a regular question type with YAML data holding the answer value.
            # Validate the value, unless the value is null.
            value = answer["answer"]
            if value is None:
                # Null answers are always acceptable.
                pass
            else:
                from guidedmodules.answer_validation import validator
                try:
                    value = validator.validate(question, value)
                except ValueError as e:
                    self.log("ERROR",
                             "Answering {}: {}".format(question.key, e))
                    return

            # Save the value.
            if taskans.save_answer(value, [], None, self.dummy_user, "api"):
                self.log(
                    "OK",
                    "Answered {} with {}.".format(question.key, repr(value)))
                return

            # No change, somehow.
            return
Ejemplo n.º 4
0
def project_api(request, org_slug, project_id):
    from collections import OrderedDict

    # Get user from API key.
    api_key = request.META.get("HTTP_AUTHORIZATION", "").strip()
    if len(
            api_key
    ) < 32:  # prevent null values from matching against users without api keys
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "An API key was not present in the Authorization header.")
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    from django.db.models import Q
    from .models import User, Project

    try:
        user = User.objects.get(
            Q(api_key_rw=api_key) | Q(api_key_ro=api_key)
            | Q(api_key_wo=api_key))
    except User.DoesNotExist:
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "A valid API key was not present in the Authorization header.")
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    # Get project and check authorization.
    organization = get_object_or_404(Organization, subdomain=org_slug)
    project = get_object_or_404(Project,
                                id=project_id,
                                organization=organization)
    if not project.has_read_priv(user):
        return JsonResponse(OrderedDict([
            ("status", "error"),
            ("error",
             "The user associated with the API key does not have read perission on the project."
             )
        ]),
                            json_dumps_params={"indent": 2},
                            status=403)

    if request.method == "GET":
        # Export data.
        value = project.export_json(include_file_content=False,
                                    include_metadata=False)

        # Return the value as JSON.
        return JsonResponse(value, json_dumps_params={"indent": 2})

    elif request.method == "POST":
        # Update the value.

        # Check that the API key has write access.
        if not project.root_task.has_write_priv(user):
            return JsonResponse(OrderedDict([
                ("status", "error"),
                ("error",
                 "The user associated with the API key does not have write perission on the project."
                 )
            ]),
                                json_dumps_params={"indent": 2},
                                status=403)
        if api_key not in (user.api_key_rw, user.api_key_wo):
            return JsonResponse(OrderedDict([
                ("status", "error"),
                ("error",
                 "The API key does not have write perission on the project.")
            ]),
                                json_dumps_params={"indent": 2},
                                status=403)

        # Parse the new project body.
        if request.META.get("CONTENT_TYPE") == "application/json":
            # A JSON object is given in the import/export format. Parse it.
            import json
            try:
                value = json.loads(request.body.decode("utf-8"))
            except json.decoder.JSONDecodeError as e:
                return JsonResponse(OrderedDict([
                    ("status", "error"),
                    ("error", "Invalid JSON in request body. " + str(e))
                ]),
                                    json_dumps_params={"indent": 2},
                                    status=400)

            # Update.
            log = []
            ok = project.import_json(value, user, "api",
                                     lambda msg: log.append(msg))

        else:
            # Form fields are given in POST and FILES. Process each item...

            from guidedmodules.models import TaskAnswer
            from guidedmodules.answer_validation import question_input_parser, validator
            import django.core.files.uploadedfile

            log = []
            ok = True
            for key, v in list(request.POST.lists()) + list(
                    request.FILES.items()):
                try:
                    # Find the Task and ModuleQuestion that corresponds to
                    # this key-value pair.
                    task = project.root_task
                    question = None
                    if not key.startswith("project."):
                        raise ValueError("Invalid question ID: " + key)
                    for pathitem in key.split(".")[1:]:
                        # Advance to the task pointed to by the last question.
                        if question is not None:
                            if question.spec["type"] != "module":
                                raise ValueError("Invalid question ID: " + key)
                            ta = task.answers.filter(question=question).first()
                            if ta is None:
                                raise ValueError(
                                    "Invalid question ID (question on path is not answered yet): "
                                    + key)
                            a = ta.get_current_answer()
                            if not a or a.cleared:
                                raise ValueError(
                                    "Invalid question ID (question on path is not answered yet): "
                                    + key)
                            task = a.answered_by_task.first()

                        # Get the question this corresponds to within the task
                        question = task.module.questions.filter(
                            key=pathitem).first()
                        if not question:
                            raise ValueError("Invalid question ID: " + key)

                    # Parse and validate the answer.
                    v = question_input_parser.parse(question, v)
                    v = validator.validate(question, v)

                    # Save.
                    v_file = None
                    if question.spec["type"] == "file":
                        v_file = v
                        v = None
                    ta, _ = TaskAnswer.objects.get_or_create(task=task,
                                                             question=question)
                    saved = ta.save_answer(v, [], v_file, user, "api")
                    if saved:
                        log.append(key + " updated")
                    else:
                        log.append(key + " unchanged")

                except ValueError as e:
                    log.append(key + ": " + str(e))
                    ok = False

        return JsonResponse(OrderedDict([
            ("status", "ok" if ok else "error"),
            ("details", log),
        ]),
                            json_dumps_params={"indent": 2})
Ejemplo n.º 5
0
    def set_answer(task, question, answer):
        # Set the answer to the question for the given task.

        if question.spec[
                "type"] == "module" and question.answer_type_module is not None:
            # If there is no answer provided, normally we leave
            # the answer blank. However, for module-type questions
            # with a module type (not a protocol), we should at least
            # *start* the sub-task with the module answer type, even if we
            # don't answer any of its questions, because it may be
            # a question-less module that only provides output documents.
            log("Starting {}...".format(question.key))
            sub_task = task.get_or_create_subtask(Command.dummy_user, question)
            if answer is not None:
                Command.set_answers(sub_task, answer.get("questions", []))
                return True
            return False

        # If the question isn't answered, leave it alone.
        if answer is None:
            return False

        # Set an answer to the question.

        # Get the TaskAnswer record, which has the save_answer function.
        taskans, isnew = TaskAnswer.objects.get_or_create(task=task,
                                                          question=question)

        if question.spec["type"] in ("module", "module-set"):
            # A module-type question with a protocol (if there was no protocol, it
            # was handled above) or a module-set question (with or without a protocol).

            if question.spec["type"] == "module":
                answers = [answer]
            else:
                answers = answer.get("answers", [])

            # Start the app(s).
            for i, answer in enumerate(answers):
                if question.answer_type_module is not None:
                    # Start the sub-task.
                    answers[i] = task.get_or_create_subtask(
                        Command.dummy_user, question)
                else:
                    # Start the app. The app to start is specified in the 'app' key.
                    if not isinstance(answer, dict):
                        raise ValueError("invalid data type")
                    project = Command.start_app(answer.get("app"))

                    # Validate that the protocols match.
                    unimplemented_protocols = set(
                        question.spec.get("protocol", [])) - set(
                            project.root_task.module.spec.get("protocol", []))
                    if unimplemented_protocols:
                        # There are unimplemented protocols.
                        log("{} doesn't implement the protocol {} required to answer {}."
                            .format(project.root_task.module.app,
                                    ", ".join(sorted(unimplemented_protocols)),
                                    question.key))
                        return False

                    # Keep the root Task to be an answer to the question.
                    answers[i] = project.root_task

                # Set answers of sub-task.
                Command.set_answers(answers[i], answer.get("questions", []))

            # Save the answer.
            if taskans.save_answer(None, answers, None, Command.dummy_user,
                                   "api"):
                log("Answered {} with {}...".format(question.key, answers))
                return True

        else:
            # This is a regular question type with YAML data holding the answer value.
            # Validate the value.
            from guidedmodules.answer_validation import validator
            try:
                value = validator.validate(question, answer["answer"])
            except ValueError as e:
                log("Answering {}: {}...".format(question.key, e))
                return False

            # Save the value.
            if taskans.save_answer(value, [], None, Command.dummy_user, "api"):
                log("Answered {} with {}...".format(question.key, answer))
                return True

            # No change, somehow.
            return False
Ejemplo n.º 6
0
    def set_answer(self, task, question, answer, update_answer_func, basedir,
                   options):
        # Set the answer to the question for the given task.

        if question.spec[
                "type"] == "module" and question.answer_type_module is not None:
            # If there is no answer provided, normally we leave
            # the answer blank. However, for module-type questions
            # with a module type (not a protocol), we should at least
            # *start* the sub-task with the module answer type, even if we
            # don't answer any of its questions, because it may be
            # a question-less module that only provides output documents.
            sub_task = task.get_or_create_subtask(self.dummy_user, question)
            # self.log("OK", "Answered {} with {}.".format(question.key, sub_task))
            if answer is None and options["add_blank_answers"]:
                answer = update_answer_func({"questions": []})
            if answer is not None:
                self.set_answers(sub_task, answer.setdefault("questions", []),
                                 basedir, options)
                return True
            return False

        # If the question isn't answered and --startapps is given on the
        # command-line, then choose an app automatically for module-type
        # questions with protocols.
        if answer is None and options["startapps"] and question.spec["type"] == "module" \
            and question.answer_type_module is None and question.spec.get("protocol"):
            # Look for an app that can be used to answer this question.
            from siteapp.views import app_satifies_interface
            for app in options["startapps"]:
                if app_satifies_interface(app["catalog_info"],
                                          question.spec.get("protocol")):
                    # We found one. Set the answer to be the path to the app,
                    # adjusted to be relative to the location of the assemble
                    # YAML file.
                    self.log(
                        "INFO",
                        "Choosing app {} for {} (from --startapps).".format(
                            app["app"], question.key))
                    answer = update_answer_func(
                        {"app": os.path.relpath(app["app"], basedir)})
                    break

        # If the question isn't answered, leave it alone.
        if answer is None:
            return False

        # Set an answer to the question.

        # Get the TaskAnswer record, which has the save_answer function.
        taskans, isnew = TaskAnswer.objects.get_or_create(task=task,
                                                          question=question)

        if question.spec["type"] in ("module", "module-set"):
            # A module-type question with a protocol (if there was no protocol, it
            # was handled above) or a module-set question (with or without a protocol).

            if question.spec["type"] == "module":
                answers = [answer]
            else:
                answers = answer.setdefault("answers", [])

            # Start the app(s).
            subtasks = []
            for answer in answers:
                if question.answer_type_module is not None:
                    # Start the sub-task.
                    subtasks.append(
                        task.get_or_create_subtask(self.dummy_user, question))
                else:
                    # Start the app. The app to start is specified in the 'app' key.
                    if not isinstance(answer, dict):
                        raise ValueError("invalid data type")
                    project = self.start_app(answer.get("app"), basedir)

                    if project is None:
                        return False  # error

                    # Validate that the protocols match.
                    unimplemented_protocols = set(
                        question.spec.get("protocol", [])) - set(
                            project.root_task.module.spec.get("protocol", []))
                    if unimplemented_protocols:
                        # There are unimplemented protocols.
                        self.log(
                            "ERROR",
                            "{} doesn't implement the protocol {} required to answer {}."
                            .format(project.root_task.module.app,
                                    ", ".join(sorted(unimplemented_protocols)),
                                    question.key))
                        return False

                    # Keep the root Task to be an answer to the question.
                    subtasks.append(project.root_task)

            # Save the answer.
            if taskans.save_answer(None, subtasks, None, self.dummy_user,
                                   "api"):
                self.log(
                    "OK", "Answered {} with {}.".format(
                        question.key,
                        ", ".join([self.str_task(t) for t in subtasks])))

                # Set answers of sub-task.
                for subtask, subtask_answers in zip(subtasks, answers):
                    self.set_answers(
                        subtask, subtask_answers.setdefault("questions", []),
                        basedir, options)

                return True

        else:
            # This is a regular question type with YAML data holding the answer value.
            # Validate the value, unless the value is null.
            value = answer["answer"]
            if value is None:
                # Null answers are always acceptable.
                pass
            else:
                from guidedmodules.answer_validation import validator
                try:
                    value = validator.validate(question, value)
                except ValueError as e:
                    self.log("ERROR",
                             "Answering {}: {}".format(question.key, e))
                    return False

            # Save the value.
            if taskans.save_answer(value, [], None, self.dummy_user, "api"):
                self.log(
                    "OK",
                    "Answered {} with {}.".format(question.key, repr(value)))
                return True

            # No change, somehow.
            return False