class ContainsText(FilterCondition): unique_name = "field_contains_text" descriptive_name = "Field contains text" field_to_match = field_utils.CharField(label="Field to match", required=True) inverse = field_utils.BooleanField(label="Flip to inverse") def __init__(self, field_to_match, text, inverse=False): self.field_to_match = field_to_match self.text = text self.inverse = inverse def validate(self, permission, target): field = self.get_matching_field(self.field_to_match, permission=permission, target=target) if not field: return False, f"No field {self.field_to_match} on target" convertible = field.can_convert_to("CharField") if not field: return False, f"Field {self.field_to_match} cannot convert to text" return True def check(self, action): field = self.get_matched_field([action, action.target]) if self.text in field: return True return False
class ChangeInverseStateChange(BaseStateChange): """State change to toggle the inverse field of a permission.""" descriptive_text = { "verb": "toggle", "default_string": "inverse field on permission", "detail_string": "inverse field on permission to {change_to}", "preposition": "for" } section = "Permissions" allowable_targets = [PermissionsItem] settable_classes = ["all_models"] change_to = field_utils.BooleanField( label="Change inverse field of permission to", required=True) def validate(self, actor, target): if not isinstance(self.change_to, bool): raise ValidationError( f"'change_to' must be True or False, not {type(self.change_to)}" ) def implement(self, actor, target, **kwargs): target.inverse = self.change_to target.save() return target
class EditPermissionStateChange(BaseStateChange): descriptive_text = { "verb": "edit", "default_string": "permission", "preposition": "on" } section = "Permissions" allowable_targets = [PermissionsItem] settable_classes = ["all_models"] model_based_validation = (PermissionsItem, ["anyone", "roles", "actors"]) actors = field_utils.ActorListField( label="Actors who have this permission", null_value=list) roles = field_utils.RoleListField(label="Roles who have this permission", null_value=list) anyone = field_utils.BooleanField(label="Everyone has the permission", null_value=False) def validate(self, actor, target): if not self.actors and not self.roles and not self.anyone: raise ValidationError( "Must change at least one field to edit permission.") def implement(self, actor, target, **kwargs): field_dict = {} if self.actors: field_dict["actors"] = self.actors if self.roles: field_dict["roles"] = self.roles if self.anyone: field_dict["anyone"] = self.anyone target.set_fields(**field_dict) target.save() return target
class ActorIsSameAs(FilterCondition): """ Note: this replaces: 'self only' (actor is the same as member_pk_list) 'original creator only' (actor is the same as target.commented_on.creator) 'commenter only' (actor is the same as target.creator) """ unique_name = "actor_is_same_as" descriptive_name = "Actor is the same as" field_to_match = field_utils.CharField(label="Field to match", required=True) inverse = field_utils.BooleanField(label="Flip to inverse") def __init__(self, field_to_match, inverse=False): self.field_to_match = field_to_match self.inverse = inverse def validate(self, permission, target): field = self.get_matching_field(self.field_to_match, permission=permission, target=target) if not field: return False, f"No field found for '{self.field_to_match}'" convertible = field.can_convert_to("ActorField") if not field: return False, f"Field {self.field_to_match} cannot convert to Actor" return True def check(self, action): field = self.get_matching_field(self.field_to_match, action=action) if action.actor == field.to_ActorField: return True return False
class FieldIs(FilterCondition): unique_name = "field_is" descriptive_name = "Field X is value Y" field_to_match = field_utils.CharField(label="Field to match", required=True) value_to_match = field_utils.CharField(label="Value to match", required=True) inverse = field_utils.BooleanField(label="Flip to inverse") def __init__(self, field_to_match, value_to_match, inverse): self.field_to_match = field_to_match self.value_to_match = value_to_match self.inverse = inverse def validate(self, permission, target): field = getattr(permission.change, self.field_to_match) if not field: return False, f"No field found for '{self.field_to_match}'" if not field.transform_to_valid_value(self.value_to_match): return False, f"{self.value_to_match} is not a valid value for {self.field_to_match}" return True, None def check(self, action): field = getattr(action.change, self.field_to_match) return field.value == self.value_to_match
class TargetType(FilterCondition): unique_name = "target_is_type" descriptive_name = "Target is of type" target_type = field_utils.PermissionedModelField(label="Limit targets to type", required=True) inverse = field_utils.BooleanField(label="Flip to inverse") def __init__(self, target_type, inverse=False): self.target_type = target_type self.inverse = inverse def check(self, action): if action.target.__class__.__name__ == self.target_type: return True return False
class ActorMemberCondition(FilterCondition): unique_name = "actor_user_age" descriptive_name = "Actor has been member of community longer than" duration = field_utils.DurationField(label="Duration of membership required", required=True) inverse = field_utils.BooleanField(label="Flip to inverse (actor has been member of community less than...)") def __init__(self, duration, inverse=False): self.duration = duration self.inverse = inverse def check(self, action): date_joined = Client(target=action.target.get_owner().Community.user_joined(action.actor)) if datetime.datetime.now() - date_joined > self.duration: return True return False
class ActorUserCondition(FilterCondition): unique_name = "actor_user_age" descriptive_name = "Actor has been user longer than" duration = field_utils.DurationField(label="Length of time that must pass", required=True) inverse = field_utils.BooleanField(label="Flip to inverse (actor has been user less than...)") def __init__(self, duration=None, inverse=False): self.duration = duration self.inverse = inverse def description_for_passing_condition(self): units = utils.parse_duration_into_units(self.duration, measured_in="seconds") time_length = utils.display_duration_units(**units) return f"actor has been user longer than {time_length}" def check(self, action): if (datetime.now(timezone.utc) - action.actor.date_joined).seconds >= self.duration: return True return False
class FieldContainsFilter(Filter): descriptive_name = "a field contains specific text" configured_name = "{field_to_match} {verb} '{value_to_match}'" field_to_match = field_utils.CharField(label="Field to look in", required=True) value_to_match = field_utils.CharField(label="Text to search for", required=True) inverse = field_utils.BooleanField(label="Reverse (only allowed if it does NOT contain above text)", default=False) # TODO: for now we can only match change object fields, probably should be more flexible - use crawl objects? # TODO: should either restrict this to text fields or find a coherent way of translating non-text fields to text def does_not_contain(self): if isinstance(self.inverse, bool): return self.inverse return False def validate(self, permission): change_obj = permission.get_state_change_object() if not hasattr(change_obj, self.field_to_match): return False, f"No field '{self.field_to_match}' on this permission" return True, None def check(self, *, action, **kwargs): """The contents of the action field should equal the custom text.""" field_value = getattr(action.change, self.field_to_match) if self.does_not_contain: failure_msg = f"field '{self.field_to_match}' contains '{self.value_to_match.lower()}'" return self.value_to_match.lower() not in field_value.lower(), failure_msg failure_msg = f"field '{self.field_to_match}' does not contain '{self.value_to_match.lower()}'" return self.value_to_match.lower() in field_value.lower(), failure_msg def get_configured_name(self): if hasattr(self, "configured_name"): text_dict = self.get_input_field_values() text_dict["verb"] = "does not contain" if self.does_not_contain() else "contains" return self.configured_name.format(**text_dict) return self.get_descriptive_name()
class AddPermissionStateChange(BaseStateChange): """State change to add a permission to something.""" descriptive_text = { # note that description_present_tense and past tense are overridden below "verb": "add", "default_string": "permission" } section = "Permissions" model_based_validation = (PermissionsItem, ["change_type", "anyone", "inverse"]) change_type = field_utils.CharField( label="Type of action the permission covers", required=True) actors = field_utils.ActorListField( label="Actors who have this permission", null_value=list) roles = field_utils.RoleListField(label="Roles who have this permission", null_value=list) anyone = field_utils.BooleanField(label="Everyone has the permission", null_value=False) inverse = field_utils.BooleanField( label="Do the inverse of this permission", null_value=False) condition_data = field_utils.CharField( label="Condition for this permission") def description_present_tense(self): return f"add permission '{get_verb_given_permission_type(self.change_type)}'" def description_past_tense(self): return f"added permission '{get_verb_given_permission_type(self.change_type)}'" def is_conditionally_foundational(self, action): """Some state changes are only foundational in certain conditions. Those state changes override this method to apply logic and determine whether a specific instance is foundational or not.""" from concord.utils.lookups import get_state_change_object change_object = get_state_change_object(self.change_type) return action.change.is_foundational def validate(self, actor, target): permission = get_state_change_object(self.change_type) # check that target is a valid class for the permission to be set on if target.__class__ not in permission.get_settable_classes(): settable_classes_str = ", ".join( [str(option) for option in permission.get_settable_classes()]) raise ValidationError( f"This kind of permission cannot be set on target {target} of class " + f"{target.__class__}, must be {settable_classes_str}") # validate condition data if self.condition_data: for condition in self.condition_data: is_valid, message = validate_condition( condition["condition_type"], condition["condition_data"], condition["permission_data"], target) if not is_valid: raise ValidationError(message) def implement(self, actor, target, **kwargs): permission = PermissionsItem() permission.set_fields(owner=target.get_owner(), permitted_object=target, anyone=self.anyone, change_type=self.change_type, inverse=self.inverse, actors=self.actors, roles=self.roles) permission.save() # if condition, add and save if self.condition_data: # create initial manager owner = permission.get_owner() manager = ConditionManager.objects.create(owner=owner, community=owner.pk, set_on="permission") permission.condition = manager # add conditions for condition in self.condition_data: data = { "condition_type": condition["condition_type"], "condition_data": condition["condition_data"], "permission_data": condition["permission_data"] } manager.add_condition(data_for_condition=data) manager.save() return permission
class ApplyTemplateStateChange(BaseStateChange): """State change object for applying a template.""" descriptive_text = { "verb": "apply", "past_tense": "applied", "default_string": "template", "preposition": "for" } pass_action = True linked_filters = ["CreatorFilter"] # Fields template_model_pk = field_utils.IntegerField( label="PK of Template to apply", required=True) supplied_fields = field_utils.DictField( label="Fields to supply when applying template", null_value=dict) template_is_foundational = field_utils.BooleanField( label="Template makes foundational changes") def validate(self, actor, target): # check template_model_pk is valid template = TemplateModel.objects.filter(pk=self.template_model_pk) if not template: raise ValidationError( f"No template in database with ID {self.template_model_pk}") # check that supplied fields match template's siupplied fields needed_field_keys = set( [key for key, value in template[0].get_supplied_fields().items()]) supplied_field_keys = set( [key for key, value in self.supplied_fields.items()]) if needed_field_keys - supplied_field_keys: missing_fields = ', '.join( list(needed_field_keys - supplied_field_keys)) raise ValidationError( f"Template needs values for fields {missing_fields}") # attempt to apply actions (but rollback commit regardless) mock_action = MockAction(actor=actor, target=target, change=self) result = template[0].template_data.apply_template( actor=actor, target=target, trigger_action=mock_action, supplied_fields=self.supplied_fields, rollback=True) if "errors" in result: raise ValidationError( f"Template errors: {'; '.join([error for error in result['errors']])}" ) def implement(self, actor, target, **kwargs): """Implements the given template, relies on logic in apply_template.""" action = kwargs.get("action", None) template_model = TemplateModel.objects.get(pk=self.template_model_pk) return template_model.template_data.apply_template( actor=actor, target=target, trigger_action=action, supplied_fields=self.supplied_fields)