コード例 #1
0
def validate_document(doc, error_message_name, app):
    # The document must be either a string which points to another
    # file holding the document, or a dictionary. But the string
    # form isn't available if we're validating a new spec submitted
    # by the authoring tool since we don't have the app virtual
    # filesystem at that point.
    if app:
        if not isinstance(doc, (str, dict)):
            raise ValidationError(error_message_name, "Must be a file name or dictionary, not a %s." % type(doc).__name__)
    else:
        if not isinstance(doc, dict):
            raise ValidationError(error_message_name, "Must be a dictionary, not a %s." % type(doc).__name__)

    # If it's a string, slurp in the document from an external file.
    # The document begins with YAML dictionary terminated by a line
    # containing three dots. The subsequent content is stored in
    # the dictionary's 'template' field. The file name is stored
    # in the 'filename' field so that we can re-generate the original
    # filesystem layout in Module::serialize_to_disk.
    if isinstance(doc, str):
        error_message_name += " ({})".format(doc)

        # Read the external file.
        blob = app.read_file(doc)

        # Split the file on the first ocurrence of three dots. This
        # is YAML's standard end-of-stream marker. But PyYAML doesn't
        # have a way to read just up to the "...", so we handle that
        # ourselves.
        sep = "\n...\n"
        if sep not in blob:
            raise ValidationError(error_message_name, "File does not contain a line with just '...'.")
        data, template = blob.split(sep, 1)

        # Parse the YAML above the "...".
        data = rtyaml.load(data)

        # Trim the template so that it looks good if the revised
        # module spec is serialized to YAML.
        template = template.rstrip() + "\n"

        # Store the filename and template in it.
        data['filename'] = doc
        data['template'] = template
        doc = data

    # Check that the template is valid.
    try:
        render_content(doc, None, "PARSE_ONLY", "(document template)")
    except KeyError as e:
        raise ValidationError(error_message_name, "Missing field: %s" % str(e))
    except ValueError as e:
        raise ValidationError(error_message_name, "Invalid template: %s" % str(e))

    return doc
def validate_module(spec, is_authoring_tool=False):
    # Validate that the introduction and output documents are renderable.
    if "introduction" in spec:
        if not isinstance(spec["introduction"], dict):
            raise ValidationError(
                "module introduction", "Must be a dictionary, not a %s." %
                str(type(spec["introduction"])))
        try:
            render_content(spec["introduction"], None, "PARSE_ONLY",
                           "(introduction)")
        except ValueError as e:
            raise ValidationError("module introduction",
                                  "Invalid Jinja2 template: " + str(e))

    if not isinstance(spec.get("output", []), list):
        raise ValidationError(
            "module output",
            "Must be a list, not a %s." % str(type(spec.get("output"))))
    for i, doc in enumerate(spec.get("output", [])):
        try:
            render_content(doc, None, "PARSE_ONLY", "(output document)")
        except ValueError as e:
            raise ValidationError("output document #%d" % (i + 1),
                                  "Invalid Jinja2 template: %s" % str(e))

    # 'introduction' fields are an alias for an interstitial
    # question that comes before all other questions, and since
    # it is first it will be asked first. Except in projects,
    # where it's just a renderable field.
    if "introduction" in spec and spec.get("type") != "project":
        q = {
            "id": "_introduction",
            "title": "Introduction",
            "type": "interstitial",
            "prompt": spec["introduction"]["template"],
        }
        spec.setdefault("questions", []).insert(0, q)

    if not is_authoring_tool:
        # Validate the questions.
        # The authoring tool does not provide questions data.
        if not isinstance(spec.get("questions"), list):
            raise ValidationError("module questions",
                                  "Invalid value for 'questions'.")
        for i, q in enumerate(spec.get("questions", [])):
            spec["questions"][i] = validate_question(spec,
                                                     spec["questions"][i])

    return spec
コード例 #3
0
 def render_markdown_field(field, output_format, **kwargs):
     template = q.spec.get(field)
     if not template:
         return None
     if not isinstance(template, str):
         raise ValueError("%s question %s %s is not a string" % (repr(q.module), q.key, field))
     return module_logic.render_content({
             "template": template,
             "format": "markdown",
         },
         answered,
         output_format,
         "%s question %s %s" % (repr(q.module), q.key, field),
         **kwargs
     )
def validate_question(mspec, spec):
    if not spec.get("id"):
        raise ValidationError("module questions",
                              "A question is missing an id.")

    def invalid(msg):
        raise ValidationError("question %s" % spec['id'], msg)

    # clone dict before updating
    spec = OrderedDict(spec)

    # Since question IDs become Jinja2 identifiers, they must be valid
    # Jinaj2 identifiers. http://jinja.pocoo.org/docs/2.9/api/#notes-on-identifiers
    if not re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", spec["id"]):
        invalid(
            "The question ID may only contain ASCII letters, numbers, and underscores, and the first character must be a letter or underscore."
        )

    # Perform type conversions, validation, and fill in some defaults in the YAML
    # schema so that the values are ready to use in the database.
    if spec.get("type") == "multiple-choice":
        # validate and type-convert min and max

        spec["min"] = spec.get("min", 0)
        if not isinstance(spec["min"], int) or spec["min"] < 0:
            invalid("min must be a positive integer")

        spec["max"] = None if ("max" not in spec) else spec["max"]
        if spec["max"] is not None:
            if not isinstance(spec["max"], int) or spec["max"] < 0:
                invalid("max must be a positive integer")

    elif spec.get("type") in ("module", "module-set"):
        if "module-id" in spec:
            # Resolve the relative module ID to an absolute path relative
            # to the root of this app. It's optional because a protocol
            # can be specified instead.
            spec["module-id"] = resolve_relative_module_id(
                mspec, spec.get("module-id"))
        elif "protocol" in spec:
            pass
        else:
            invalid("Question must have a module-id or protocol field.")

    elif spec.get("type") == None:
        invalid("Question is missing a type.")

    # Check that required fields are present.
    if spec.get("prompt") is None:
        # Prompts are optional in project and system modules but required elsewhere.
        if mspec.get("type") not in ("project", "system-project"):
            invalid("Question prompt is missing.")

    # Check that the prompt, placeholder, and default are valid Jinja2 templates.
    for field in ("prompt", "placeholder", "default"):
        if field not in spec: continue
        if not isinstance(spec.get(field), str):
            invalid("Question %s must be a string, not a %s." %
                    (field, str(type(spec.get(field)))))
        try:
            render_content({
                "format": "markdown",
                "template": spec[field],
            }, None, "PARSE_ONLY", "(question %s)" % field)
        except ValueError as e:
            invalid("Question %s is an invalid Jinja2 template: %s" %
                    (field, e))

    # Validate impute conditions.
    imputes = spec.get("impute", [])
    if not isinstance(imputes, list):
        invalid("Impute's value must be a list.")
    for i, rule in enumerate(imputes):

        def invalid_rule(msg):
            raise ValidationError(
                mspec['id'] + " question %s, impute condition %d" %
                (spec['id'], i + 1), msg)

        # Check that the condition is a string, and that it's a valid Jinja2 expression.
        from jinja2.sandbox import SandboxedEnvironment
        env = SandboxedEnvironment()
        if "condition" in rule:
            if not isinstance(rule.get("condition"), str):
                invalid_rule("Impute condition must be a string, not a %s." %
                             str(type(rule["condition"])))
            try:
                env.compile_expression(rule["condition"])
            except Exception as e:
                invalid_rule(
                    "Impute condition %s is an invalid Jinja2 expression: %s."
                    % (repr(rule["condition"]), str(e)))

        # Check that the value is valid. If the value-mode is raw, which
        # is the default, then any Python/YAML value is valid. We only
        # check expression values.
        if rule.get("value-mode") == "expression":
            try:
                env.compile_expression(rule["value"])
            except Exception as e:
                invalid_rule(
                    "Impute condition value %s is an invalid Jinja2 expression: %s."
                    % (repr(rule["value"]), str(e)))
        if rule.get("value-mode") == "template":
            try:
                env.from_string(rule["value"])
            except Exception as e:
                invalid_rule(
                    "Impute condition value %s is an invalid Jinja2 template: %s."
                    % (repr(rule["value"]), str(e)))

    return spec
コード例 #5
0
ファイル: views_helper.py プロジェクト: GovReady/govready-q
def render_app_catalog_entry(appversion, appversions, organization):
    from guidedmodules.module_logic import render_content
    from guidedmodules.models import image_to_dataurl

    key = "{source}/{name}".format(source=appversion.source.slug,
                                   name=appversion.appname)

    catalog = appversion.catalog_metadata
    if not isinstance(catalog, dict): catalog = {}

    app_module = appversion.modules.filter(module_name="app").first()

    return {
        # app identification
        "appversion_id":
        appversion.id,
        "appsource_id":
        appversion.source.id,
        "key":
        key,

        # main display fields
        "title":
        catalog.get('title') or appversion.appname,
        "description": {  # rendered as markdown
            "short":
            render_content(
                {
                    "template": catalog.get("description", {}).get("short")
                    or "",
                    "format": "markdown",
                }, None, "html", "%s %s" % (key, "short description")),
            "long":
            render_content(
                {
                    "template":
                    catalog.get("description", {}).get("long")
                    or catalog.get("description", {}).get("short") or "",
                    "format":
                    "markdown",
                }, None, "html", "%s %s" % (key, "short description"))
        },

        # catalog page metadata
        "categories":
        catalog.get("categories", [catalog.get("category")]),
        "search_haystak":
        "".join([  # free text search uses this
            appversion.appname,
            catalog.get('title', ""),
            catalog.get("vendor", ""),
            catalog.get("description", {}).get("short", ""),
            catalog.get("description", {}).get("long", ""),
        ]),
        # "icon": None if "icon" not in catalog
        # else image_to_dataurl(appversion.get_asset(catalog["icon"]), 128),
        "icon":
        None,
        "protocol":
        app_module.spec.get("protocol", []) if app_module else [],

        # catalog detail page metadata
        "vendor":
        catalog.get("vendor"),
        "vendor_url":
        catalog.get("vendor_url"),
        "source_url":
        catalog.get("source_url"),
        "status":
        catalog.get("status"),
        "version":
        appversion.version_number,
        "recommended_for":
        catalog.get("recommended_for", []),

        # versions that can be started
        "versions":
        appversions,

        # organizations that can launch this app
        "organizations": {organization},

        # placeholder for future logic
        "authz":
        "none",
    }
コード例 #6
0
def validate_module(spec, is_authoring_tool=False):
    # Validate that the introduction and output documents are renderable.
    if "introduction" in spec:
        if not isinstance(spec["introduction"], dict):
            raise ValidationError(
                "module introduction", "Must be a dictionary, not a %s." %
                type(spec["introduction"]).__name__)
        try:
            render_content(spec["introduction"], None, "PARSE_ONLY",
                           "(introduction)")
        except ValueError as e:
            raise ValidationError("module introduction",
                                  "Invalid Jinja2 template: " + str(e))

    if not isinstance(spec.get("output", []), list):
        raise ValidationError(
            "module output",
            "Must be a list, not a %s." % type(spec.get("output")).__name__)
    for i, doc in enumerate(spec.get("output", [])):
        try:
            render_content(doc, None, "PARSE_ONLY", "(output document)")
        except ValueError as e:
            raise ValidationError("output document #%d" % (i + 1),
                                  "Invalid Jinja2 template: %s" % str(e))

    # 'introduction' fields are an alias for an interstitial
    # question that comes before all other questions, and since
    # it is first it will be asked first. Except in projects,
    # where it's just a renderable field.
    if "introduction" in spec and spec.get("type") != "project":
        q = {
            "id": "_introduction",
            "title": "Introduction",
            "type": "interstitial",
            "prompt": spec["introduction"]["template"],
        }
        spec.setdefault("questions", []).insert(0, q)

    # Validate an app protocol.
    if "protocol" in spec:
        if spec.get("type") != "project":
            raise ValidationError(
                "module specification",
                "A protocol cannot be specified in this type of module.")
        if isinstance(spec["protocol"], str):
            # If a single protocol is given, turn it into a list of one.
            spec["protocol"] = [spec["protocol"]]
        elif isinstance(spec["protocol"], list):
            # If it's a list, the values must be strings.
            for x in spec["protocol"]:
                if not isinstance(x, str):
                    raise ValidationError(
                        "protocols",
                        "Protocols must be strings (not %s)." % repr(x))
        else:
            raise ValidationError(
                "module specification",
                "protocol must be a string or a list of strings (not %s)." %
                repr(spec["protocol"]))

    # Validate the questions.
    if not isinstance(spec.get("questions"), (type(None), list)):
        raise ValidationError("module questions",
                              "Invalid data type of value for 'questions'.")
    for i, q in enumerate(spec.get("questions", [])):
        spec["questions"][i] = validate_question(spec, spec["questions"][i])

    return spec