Beispiel #1
0
    def validate_arguments(self, value):
        # Get the schema associated with the selected action
        try:
            schema = Action.objects.get(
                name=self.initial_data.get('action')).arguments_schema
        except:
            raise serializers.ValidationError(
                'Could not find arguments schema.')

        schemaValidator = JSONSchemaValidator(schema)
        errorResponse = {}
        errors = sorted(schemaValidator.iter_errors(value),
                        key=lambda e: e.path)

        # Loop through ValidationErrors returned by JSONSchema
        # Each error contains a message and a path attribute
        # message: string human-readable error explanation
        # path: list containing path to offending element
        for error in errors:
            currentLevel = errorResponse

            # Loop through the path of the current error
            # e.g. ['surveys'][0]['weight']
            for index, path in enumerate(error.path):
                # If this key already exists in our error response, step into it
                if path in currentLevel:
                    currentLevel = currentLevel[path]
                    continue
                else:
                    # If we haven't reached the end of the path, add this path
                    # as a key in our error response object and step into it
                    if index < len(error.path) - 1:
                        currentLevel[path] = {}
                        currentLevel = currentLevel[path]
                        continue
                    # If we've reached the final path, set the error message
                    else:
                        currentLevel[path] = error.message

        if (errorResponse):
            raise serializers.ValidationError(errorResponse)

        return value
Beispiel #2
0
    def validate_arguments(self, value):
        # Get the schema associated with the selected action
        try:
            schema = Action.objects.get(name=self.initial_data.get('action')).arguments_schema
        except:
            raise serializers.ValidationError('Could not find arguments schema.')

        schemaValidator = JSONSchemaValidator(schema)
        errorResponse = {}
        errors = sorted(schemaValidator.iter_errors(value), key=lambda e: e.path)

        # Loop through ValidationErrors returned by JSONSchema
        # Each error contains a message and a path attribute
        # message: string human-readable error explanation
        # path: list containing path to offending element
        for error in errors:
            currentLevel = errorResponse

            # Loop through the path of the current error
            # e.g. ['surveys'][0]['weight']
            for index, path in enumerate(error.path):
                # If this key already exists in our error response, step into it
                if path in currentLevel:
                    currentLevel = currentLevel[path]
                    continue
                else:
                    # If we haven't reached the end of the path, add this path
                    # as a key in our error response object and step into it
                    if index < len(error.path) - 1:
                        currentLevel[path] = {}
                        currentLevel = currentLevel[path]
                        continue
                    # If we've reached the final path, set the error message
                    else:
                        currentLevel[path] = error.message

        if (errorResponse):
            raise serializers.ValidationError(errorResponse)

        return value
Beispiel #3
0
    def validate(self, data):
        data = super().validate(data)
        action = data.get("action")
        if action is None:
            action = self.instance.latest_revision.action

        arguments = data.get("arguments")
        if arguments is not None:
            # Ensure the value is a dict
            if not isinstance(arguments, dict):
                raise serializers.ValidationError({"arguments": "Must be an object."})

            # Get the schema associated with the selected action
            schema = action.arguments_schema

            schemaValidator = JSONSchemaValidator(schema)
            errorResponse = {}
            errors = sorted(schemaValidator.iter_errors(arguments), key=lambda e: e.path)

            # Loop through ValidationErrors returned by JSONSchema
            # Each error contains a message and a path attribute
            # message: string human-readable error explanation
            # path: list containing path to offending element
            for error in errors:
                currentLevel = errorResponse

                # Loop through the path of the current error
                # e.g. ['surveys'][0]['weight']
                for index, path in enumerate(error.path):
                    # If this key already exists in our error response, step into it
                    if path in currentLevel:
                        currentLevel = currentLevel[path]
                        continue
                    else:
                        # If we haven't reached the end of the path, add this path
                        # as a key in our error response object and step into it
                        if index < len(error.path) - 1:
                            currentLevel[path] = {}
                            currentLevel = currentLevel[path]
                            continue
                        # If we've reached the final path, set the error message
                        else:
                            currentLevel[path] = error.message

            if errorResponse:
                raise serializers.ValidationError({"arguments": errorResponse})

        if self.instance is None:
            if data.get("extra_filter_expression", "").strip() == "":
                if not data.get("filter_object"):
                    raise serializers.ValidationError(
                        "one of extra_filter_expression or filter_object is required"
                    )
        else:
            if "extra_filter_expression" in data or "filter_object" in data:
                # If either is attempted to be updated, at least one of them must be truthy.
                if not data.get("extra_filter_expression", "").strip() and not data.get(
                    "filter_object"
                ):
                    raise serializers.ValidationError(
                        "if extra_filter_expression is blank, "
                        "at least one filter_object is required"
                    )

        return data
Beispiel #4
0
    def validate_arguments(self, arguments, revision):
        """
        Test if `arguments` follows all action-specific rules.

        Raises `ValidationError` if any rules are violated.
        """

        # Make a default dict that always returns a default dict
        def default():
            return defaultdict(default)

        errors = default()

        # Check for any JSON Schema violations
        schemaValidator = JSONSchemaValidator(self.arguments_schema)
        for error in schemaValidator.iter_errors(arguments):
            current_level = errors
            path = list(error.path)
            for part in path[:-1]:
                current_level = current_level[part]
            current_level[path[-1]] = error.message

        if errors:
            raise serializers.ValidationError({"arguments": errors})

        if self.name == "preference-experiment":
            # Feature branch slugs should be unique within an experiment.
            branch_slugs = set()
            branch_values = set()
            for i, branch in enumerate(arguments.get("branches")):
                if branch["slug"] in branch_slugs:
                    msg = self.errors["duplicate_branch_slug"]
                    errors["branches"][i]["slug"] = msg

                if branch["value"] in branch_values:
                    msg = self.errors["duplicate_branch_value"]
                    errors["branches"][i]["value"] = msg

                branch_slugs.add(branch["slug"])
                branch_values.add(branch["value"])

            # Experiment slugs should be unique.
            experiment_recipes = Recipe.objects.filter(
                latest_revision__action=self)
            if revision.recipe and revision.recipe.id:
                experiment_recipes = experiment_recipes.exclude(
                    id=revision.recipe.id)
            existing_slugs = set(
                r.latest_revision.arguments.get("slug")
                for r in experiment_recipes)
            if arguments.get("slug") in existing_slugs:
                msg = self.errors["duplicate_experiment_slug"]
                errors["slug"] = msg

        elif self.name == "preference-rollout":
            # Rollout slugs should be unique
            rollout_recipes = Recipe.objects.filter(
                latest_revision__action=self)
            if revision.recipe and revision.recipe.id:
                rollout_recipes = rollout_recipes.exclude(
                    id=revision.recipe.id)
            existing_slugs = set(
                r.latest_revision.arguments.get("slug")
                for r in rollout_recipes)
            if arguments.get("slug") in existing_slugs:
                msg = self.errors["duplicate_rollout_slug"]
                errors["slug"] = msg

        elif self.name == "preference-rollback":
            # Rollback slugs should match rollouts
            rollouts = Recipe.objects.filter(
                latest_revision__action__name="preference-rollout")
            rollout_slugs = set(r.latest_revision.arguments["slug"]
                                for r in rollouts)
            if arguments["rolloutSlug"] not in rollout_slugs:
                errors["slug"] = self.errors["rollout_slug_not_found"]

        elif self.name == "show-heartbeat":
            # Survey ID should be unique across all recipes
            other_recipes = Recipe.objects.filter(latest_revision__action=self)
            if revision.recipe and revision.recipe.id:
                other_recipes = other_recipes.exclude(id=revision.recipe.id)
            # So it *could* be that a different recipe's *latest_revision*'s argument
            # has this same surveyId but its *approved_revision* has a different surveyId.
            # It's unlikely in the real-world that different revisions, within a recipe,
            # has different surveyIds *and* that any of these clash with an entirely
            # different recipe.
            for recipe in other_recipes:
                if recipe.latest_revision.arguments["surveyId"] == arguments[
                        "surveyId"]:
                    errors["surveyId"] = self.errors["duplicate_survey_id"]

        elif self.name == "opt-out-study":
            # Name should be unique across all recipes
            other_recipes = Recipe.objects.filter(latest_revision__action=self)
            if revision.recipe and revision.recipe.id:
                other_recipes = other_recipes.exclude(id=revision.recipe.id)
            for recipe in other_recipes:
                if recipe.latest_revision.arguments["name"] == arguments[
                        "name"]:
                    errors["name"] = self.errors["duplicate_study_name"]

        # Raise errors, if any
        if errors:
            raise serializers.ValidationError({"arguments": errors})
Beispiel #5
0
    def revise(self, force=False, **data):
        revision = self.latest_revision

        if "arguments" in data:
            arguments = data.pop("arguments")
            data["arguments_json"] = json.dumps(arguments)
        else:
            arguments = None

        if "filter_object" in data:
            data["filter_object_json"] = json.dumps(data.pop("filter_object"))

        if revision:
            revisions = RecipeRevision.objects.filter(id=revision.id)

            revision_data = revision.data
            revision_data.update(data)

            channels = revision_data.pop("channels")
            revisions = filter_m2m(revisions, "channels", channels)

            countries = revision_data.pop("countries")
            revisions = filter_m2m(revisions, "countries", countries)

            locales = revision_data.pop("locales")
            revisions = filter_m2m(revisions, "locales", locales)

            data = revision_data
            revisions = revisions.filter(**data)

            is_clean = revisions.exists()
        else:
            channels = data.pop("channels", [])
            countries = data.pop("countries", [])
            locales = data.pop("locales", [])
            is_clean = False

        if arguments is not None:
            schema = None
            if "action_id" in data:
                schema = Action.objects.get(
                    action_id=data["action_id"]).arguments_schema
            elif revision:
                schema = revision.action.arguments_schema

            if schema is not None:
                schema_validator = JSONSchemaValidator(schema)
                schema_validator.validate(arguments)

        if not is_clean or force:
            logger.info(
                f"Creating new revision for recipe ID [{self.id}]",
                extra={"code": INFO_CREATE_REVISION},
            )

            if revision and revision.approval_status == RecipeRevision.PENDING:
                revision.approval_request.delete()

            self.latest_revision = RecipeRevision.objects.create(
                recipe=self, parent=revision, **data)

            for channel in channels:
                self.latest_revision.channels.add(channel)

            for country in countries:
                self.latest_revision.countries.add(country)

            for locale in locales:
                self.latest_revision.locales.add(locale)

            self.save()
Beispiel #6
0
    def validate(self, data):
        data = super().validate(data)
        action = data.get("action")
        if action is None:
            action = self.instance.action

        arguments = data.get("arguments")
        if arguments is not None:
            # Ensure the value is a dict
            if not isinstance(arguments, dict):
                raise serializers.ValidationError({"arguments": "Must be an object."})

            # Get the schema associated with the selected action
            schema = action.arguments_schema

            schemaValidator = JSONSchemaValidator(schema)
            errorResponse = {}
            errors = sorted(schemaValidator.iter_errors(arguments), key=lambda e: e.path)

            # Loop through ValidationErrors returned by JSONSchema
            # Each error contains a message and a path attribute
            # message: string human-readable error explanation
            # path: list containing path to offending element
            for error in errors:
                currentLevel = errorResponse

                # Loop through the path of the current error
                # e.g. ['surveys'][0]['weight']
                for index, path in enumerate(error.path):
                    # If this key already exists in our error response, step into it
                    if path in currentLevel:
                        currentLevel = currentLevel[path]
                        continue
                    else:
                        # If we haven't reached the end of the path, add this path
                        # as a key in our error response object and step into it
                        if index < len(error.path) - 1:
                            currentLevel[path] = {}
                            currentLevel = currentLevel[path]
                            continue
                        # If we've reached the final path, set the error message
                        else:
                            currentLevel[path] = error.message

            if errorResponse:
                raise serializers.ValidationError({"arguments": errorResponse})

        if self.instance is None:
            if data.get("extra_filter_expression", "").strip() == "":
                if not data.get("filter_object"):
                    raise serializers.ValidationError(
                        "one of extra_filter_expression or filter_object is required"
                    )
        else:
            if "extra_filter_expression" in data or "filter_object" in data:
                # If either is attempted to be updated, at least one of them must be truthy.
                if not data.get("extra_filter_expression", "").strip() and not data.get(
                    "filter_object"
                ):
                    raise serializers.ValidationError(
                        "if extra_filter_expression is blank, "
                        "at least one filter_object is required"
                    )

        return data