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
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
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
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})
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()
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