class AddPostStateChange(BaseStateChange): descriptive_text = { "verb": "add", "default_string": "post", "detail_string": "post with title '{title}'" } section = "Forum" input_target = Post allowable_targets = [Forum] settable_classes = ["all_community_models", Forum] model_based_validation = (Post, ["title", "content"]) # Fields title = field_utils.CharField(label="Title", required=True) content = field_utils.CharField(label="Content", required=True) def implement(self, actor, target, action): post = Post.objects.create(title=self.title, content=self.content, author=actor, owner=target.get_owner(), forum=target) self.set_default_permissions(actor, post) return post
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 EditPostStateChange(BaseStateChange): descriptive_text = { "verb": "edit", "default_string": "post", "preposition": "in" } section = "Forum" context_keys = ["forum", "post"] allowable_targets = [Post] settable_classes = ["all_community_models", Forum, Post] linked_filters = ["CreatorOnly"] model_based_validation = (Post, ["title", "content"]) title = field_utils.CharField(label="Title") content = field_utils.CharField(label="Content") def get_context_instances(self, action): """Returns the forum and the post object.""" return {"post": action.target, "forum": action.target.forum} def validate(self, actor, target): if not self.title and not self.content: raise ValidationError( "Must provide either a new title or new content when editing post" ) def implement(self, actor, target, action): target.title = self.title if self.title else target.title target.content = self.content if self.content else target.content target.save() return target
class EditGroupStateChange(BaseStateChange): descriptive_text = { "verb": "edit", "default_string": "group", "preposition": "for" } section = "Community" allowable_targets = [Group] name = field_utils.CharField(label="Group name", required=False) description = field_utils.CharField(label="Group description", required=False) def validate(self, actor, target): if not self.name and not self.description: raise ValidationError( "Must provide either a new name or a new description") def implement(self, actor, target, action): if self.description: target.group_description = self.description if self.name: target.name = self.name target.save() return target
class AddColumnStateChange(BaseStateChange): """State change to add column to a list.""" descriptive_text = { "verb": "add", "default_string": "column to list", "detail_string": "column '{column_name}' to list" } section = "List" allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] # Fields column_name = field_utils.CharField(label="Name of column", required=True) required = field_utils.CharField(label="Is column required") default_value = field_utils.CharField(label="Default value of column") def validate(self, actor, target): target.add_column(**self.get_field_data(with_unset=False)) def implement(self, actor, target, **kwargs): target.add_column(**self.get_field_data(with_unset=False)) target.save() return target
class AddForumStateChange(BaseStateChange): descriptive_text = { "verb": "add", "default_string": "forum", "detail_string": "forum '{name}'", "preposition": "on" } section = "Forum" input_target = Forum # is this vestigial? allowable_targets = ["all_community_models"] settable_classes = ["all_community_models", Forum] # Fields name = field_utils.CharField(label="Name of forum", required=True) description = field_utils.CharField(label="Forum description", null_value="") def implement(self, actor, target, action): forum = Forum.objects.create(name=self.name, description=self.description, owner=target.get_owner()) self.set_default_permissions(actor, forum) return forum
class EditListStateChange(BaseStateChange): """State Change to edit an existing list.""" descriptive_text = { "verb": "edit", "default_string": "list", "detail_string": "list to have name '{name}' and description '{description}'" } section = "List" model_based_validation = ("target", ["name", "description"]) allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] # Fields name = field_utils.CharField(label="Name") description = field_utils.CharField(label="Description") def validate(self, actor, target): if not self.name and not self.description: raise ValidationError("Must supply new name or description when editing List.") def implement(self, actor, target, **kwargs): target.name = self.name if self.name else target.name target.description = self.description if self.description else target.description target.save() return target
class EditForumStateChange(BaseStateChange): descriptive_text = { "verb": "edit", "default_string": "forum", "preposition": "in" } section = "Forum" allowable_targets = [Forum] settable_classes = ["all_community_models", Forum] name = field_utils.CharField(label="Name of forum") description = field_utils.CharField(label="Forum description") def validate(self, actor, target): if not self.name and not self.description: raise ValidationError( "Must provide either a new name or a new description") def implement(self, actor, target, action): target.name = self.name if self.name else target.name target.description = self.description if self.description else target.description target.save() return target
class EditDocumentStateChange(BaseStateChange): descriptive_text = { "verb": "edit", "default_string": "document" } section = "Document" model_based_validation = (Document, ["name", "description", "content"]) allowable_targets = [Document] settable_classes = ["all_community_models", Document] # Fields name = field_utils.CharField(label="Name") description = field_utils.CharField(label="Description") content = field_utils.CharField(label="Content") def validate(self, actor, target): if not self.name and not self.description and not self.content: raise ValidationError("Must edit name, description or content") def implement(self, actor, target, **kwargs): target.name = self.name if self.name else target.name target.description = self.description if self.description else target.description target.content = self.content if self.content else target.content target.save() return target
class AddCommentStateChange(BaseStateChange): """State Change to add a comment.""" descriptive_text = { "verb": "add", "default_string": "comment" } section = "Comment" model_based_validation = (Comment, ["text"]) context_keys = ["commented_object"] linked_filters = ["TargetTypeFilter", "CreatorOfCommentedFilter"] # Fields text = field_utils.CharField(label="Comment text", required=True) @classmethod def get_context_instances(cls, action): """Returns the commented object by its model name, to handle cases where the referer knows the model type vs doesn't know the model type.""" commented_object = action.target model_name = commented_object.__class__.__name__.lower() return {"commented_object": commented_object, model_name: commented_object} def implement(self, actor, target, **kwargs): comment = Comment(text=self.text, commenter=actor) comment.commented_object = target comment.owner = target.get_owner() comment.save() self.set_default_permissions(actor, comment) return comment
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 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 EditCommentStateChange(BaseStateChange): """State Change to edit a comment.""" descriptive_text = { "verb": "edit", "default_string": "comment" } section = "Comment" context_keys = ["comment", "commented_object"] model_based_validation = (Comment, ["text"]) linked_filters = ["CommenterFilter", "CreatorOfCommentedFilter"] allowable_targets = [Comment] settable_classes = ["all_models"] # Fields text = field_utils.CharField(label="Comment text", required=True) @classmethod def get_context_instances(cls, action): """Returns the comment and the commented object. Also returns the commented object by its model name, to handle cases where the referer knows the model type vs doesn't know the model type.""" comment = action.target commented_object = action.target.commented_object model_name = commented_object.__class__.__name__.lower() return {"comment": comment, "commented_object": commented_object, model_name: commented_object} def implement(self, actor, target, **kwargs): target.text = self.text target.save() return target
class EditTemplateStateChange(BaseStateChange): """State change to edit a template.""" descriptive_text = { "verb": "edit", "default_string": "template", "detail_string": "template field {field_name} to new value {new_field_data}" } allowable_targets = [TemplateModel] settable_classes = ["all_models"] template_object_id = field_utils.IntegerField( label="ID of Template to edit", required=True) field_name = field_utils.CharField(label="Field to edit", required=True) new_field_data = field_utils.DictField(label="Data to edit", required=True) def validate(self, actor, target): result = target.data.update_field(self.template_object_id, self.field_name, self.new_field_data) if result.__class__.__name__ == "ValidationError": raise result def implement(self, actor, target, **kwargs): target.data.update_field(self.template_object_id, self.field_name, self.new_field_data) target.save() return target
class RespondConsensusStateChange(BaseStateChange): """State change for responding to a consensus condition""" descriptive_text = { "verb": "respond", "default_string": "", "detail_string": "with {response}", "preposition": "" } section = "Consensus" allowable_targets = [ConsensusCondition] response = field_utils.CharField(label="Response", required=True) def validate(self, actor, target): """Checks that the actor is a participant.""" if self.response not in target.response_choices: raise ValidationError( f"Response must be one of {', '.join(target.response_choices)}, not {self.response}") def implement(self, actor, target, **kwargs): target.add_response(actor, self.response) target.save() return self.response
class AddVoteStateChange(BaseStateChange): """State change for adding a vote.""" descriptive_text = { "verb": "vote", "default_string": "", "detail_string": "{vote}" } section = "Vote" allowable_targets = [VoteCondition] vote = field_utils.CharField(label="Vote", required=True) def validate(self, actor, target): """ To validate the vote, we need to check that: a) the voter hasn't voted before b) if the vote is abstain, abstentions are allowed """ if self.vote not in ["yea", "nay", "abstain"]: raise ValidationError(f"Vote type must be 'yea', 'nay' or 'abstain', not {self.vote}") if target.has_voted(actor): raise ValidationError("Actor may only vote once") if not target.allow_abstain and self.vote == "abstain": raise ValidationError("Actor abstained but this vote does not allow abstentions.") def implement(self, actor, target, **kwargs): target.add_vote(self.vote) target.add_vote_record(actor) target.save() return True
class RemoveConditionStateChange(BaseStateChange): """State change to remove condition from Community.""" descriptive_text = { "verb": "remove", "default_string": "condition", "detail_string": "condition with {element_id}" } is_foundational = True section = "Leadership" allowable_targets = ["all_community_models", PermissionsItem] element_id = field_utils.IntegerField(label="Element ID to remove") leadership_type = field_utils.CharField(label="Leadership type to remove condition from") def is_conditionally_foundational(self, action): """Edit condition is foundational when the condition is owner/governor.""" return self.leadership_type in ["owner", "governor"] def validate(self, actor, target): if hasattr(target, "is_community") and target.is_community and not self.leadership_type: raise ValidationError("leadership_type cannot be None") def implement(self, actor, target, **kwargs): attr_name = "condition" if not self.leadership_type else self.leadership_type + "_condition" manager = getattr(target, attr_name) if self.element_id: manager.remove_condition(self.element_id) manager.save() return manager else: manager.delete()
class FieldMatchesFilter(Filter): descriptive_name = "a field matches a value" configured_name = "{field_to_match} matches '{value_to_match}'" field_to_match = field_utils.CharField(label="Field to match", required=True) value_to_match = field_utils.CharField(label="Value to match", required=True) # TODO: for now we can only match change object fields, probably should be more flexible - use crawl objects? 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) return field_value == self.value_to_match, "field does not match"
class EditConditionStateChange(BaseStateChange): """State change to add condition to permission or leadership role.""" descriptive_text = { "verb": "edit", "default_string": "condition", "detail_string": "condition with {element_id}" } section = "Permissions" allowable_targets = ["all_community_models", PermissionsItem] element_id = field_utils.IntegerField(label="Element ID", required=True) condition_data = field_utils.DictField(label="New condition data", null_value=dict) permission_data = field_utils.DictField(label="Data for permissions set on condition", null_value=list) leadership_type = field_utils.CharField(label="Leadership type to set condition on") def is_conditionally_foundational(self, action): """Edit condition is foundational when the condition is owner/governor.""" return self.leadership_type in ["owner", "governor"] def validate(self, actor, target): if hasattr(target, "is_community") and target.is_community: if not self.leadership_type: raise ValidationError("leadership_type cannot be None") if self.leadership_type not in ["owner", "governor"]: raise ValidationError("leadership_type must be 'owner' or 'governor'") if self.leadership_type: condition_manager = getattr(target, self.leadership_type + "_condition") else: condition_manager = target.condition element = condition_manager.get_condition_data(self.element_id) is_valid, message = validate_condition( element.condition_type, self.condition_data, self.permission_data, target) if not is_valid: raise ValidationError(message) def implement(self, actor, target, **kwargs): attr_name = "condition" if not self.leadership_type else self.leadership_type + "_condition" manager = getattr(target, attr_name) data = {"condition_data": self.condition_data, "permission_data": self.permission_data} manager.edit_condition(element_id=self.element_id, data_for_condition=data) manager.save() return manager
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 AddListStateChange(BaseStateChange): """State Change to create a list in a community (or other target).""" descriptive_text = { "verb": "add", "default_string": "list", "detail_string": "list with {name}" } section = "List" model_based_validation = (SimpleList, ["name", "description"]) allowable_targets = ["all_community_models"] # Fields name = field_utils.CharField(label="Name", required=True) description = field_utils.CharField(label="Description") def implement(self, actor, target, **kwargs): simple_list = SimpleList(name=self.name, owner=target.get_owner(), creator=actor) if self.description: simple_list.description = self.description simple_list.save() self.set_default_permissions(actor, simple_list) return simple_list
class EditRowStateChange(BaseStateChange): """State Change to edit a row in a list.""" descriptive_text = { "verb": "edit", "default_string": "row in list", "detail_string": "row with ID {unique_id} to have new content {row_content}" } section = "List" allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] row_content = field_utils.CharField(label="Content of row", required=True) unique_id = field_utils.CharField(label="Unique ID of row", required=True) def validate(self, actor, target): target.edit_row(self.row_content, self.unique_id) target.refresh_from_db() def implement(self, actor, target, **kwargs): target.edit_row(self.row_content, self.unique_id) target.save() return target
class CreateDocumentStateChange(BaseStateChange): descriptive_text = { "verb": "add", "default_string": "document", "detail_string": "document with name '{name}'" } section = "Document" model_based_validation = (Document, ["name", "description", "content"]) allowable_targets = ["all_community_models"] # Fields name = field_utils.CharField(label="Name", required=True) description = field_utils.CharField(label="Description") content = field_utils.CharField(label="Content") def implement(self, actor, target, **kwargs): doc = Document(name=self.name, owner=target.get_owner(), creator=actor) doc.description = self.description if self.description else doc.description doc.content = self.content if self.content else doc.content doc.save() self.set_default_permissions(actor, doc) return doc
class ChangeNameStateChange(BaseStateChange): """State change to change name of Community.""" descriptive_text = { "verb": "change", "default_string": "name of community", "detail_string": "nameof community to {name}", "preposition": "for" } section = "Community" model_based_validation = ("target", ["name"]) allowable_targets = ["all_community_models"] name = field_utils.CharField(label="New name", required=True) def implement(self, actor, target, **kwargs): target.name = self.name target.save() return target
class DeleteRowStateChange(BaseStateChange): """State Change to delete a row in a list.""" descriptive_text = { "verb": "delete", "default_string": "row in list", "detail_string": "row with id {unique_id}" } section = "List" allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] # Fields unique_id = field_utils.CharField(label="Unique ID of row to delete", required=True) def implement(self, actor, target, **kwargs): target.delete_row(self.unique_id) target.save() return target
class DeleteColumnStateChange(BaseStateChange): descriptive_text = { "verb": "delete", "default_string": "column from list", "detail_string": "column '{column_name}' from list" } section = "List" allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] # Fields column_name = field_utils.CharField(label="Name of column", required=True) def validate(self, actor, target): target.delete_column(self.column_name) def implement(self, actor, target, **kwargs): target.delete_column(self.column_name) target.save() return target
class AddRowStateChange(BaseStateChange): """State Change to add a row to a list.""" descriptive_text = { "verb": "add", "default_string": "row to list", "detail_string": "row with content {row_content} to list" } section = "List" allowable_targets = [SimpleList] settable_classes = ["all_community_models", SimpleList] # Fields row_content = field_utils.CharField(label="Content of row", required=True) def validate(self, actor, target): target.add_row(self.row_content) target.refresh_from_db() def implement(self, actor, target, **kwargs): unique_id = target.add_row(self.row_content) target.save() return (target, unique_id)
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 AddConditionStateChange(BaseStateChange): """State change to add condition to permission or leadership role.""" descriptive_text = { "verb": "add", "default_string": "condition", "detail_string": "condition {condition_type}" } section = "Permissions" allowable_targets = ["all_community_models", PermissionsItem] condition_type = field_utils.CharField(label="Type of condition to add", required=True) condition_data = field_utils.DictField(label="Data for condition", null_value=dict) permission_data = field_utils.DictField(label="Data for permissions set on condition", null_value=list) leadership_type = field_utils.CharField(label="Type of leadership condition is set on") def is_conditionally_foundational(self, action): """Edit condition is foundational when the condition is owner/governor.""" return self.leadership_type in ["owner", "governor"] def validate(self, actor, target): if not self.condition_type: raise ValidationError("condition_type cannont be None") if not Client().Conditional.is_valid_condition_type(self.condition_type): raise ValidationError(f"condition_type must be a valid condition class not {self.condition_type}") if hasattr(target, "is_community") and target.is_community: if not self.leadership_type: raise ValidationError("leadership_type cannot be None") if self.leadership_type not in ["owner", "governor"]: raise ValidationError("leadership_type must be 'owner' or 'governor'") is_valid, message = validate_condition(self.condition_type, self.condition_data, self.permission_data, target) if not is_valid: raise ValidationError(message) def implement(self, actor, target, **kwargs): attr_name = "condition" if not self.leadership_type else self.leadership_type + "_condition" manager = getattr(target, attr_name) if not manager: from concord.conditionals.models import ConditionManager owner = target.get_owner() set_on = self.leadership_type if self.leadership_type else "permission" manager = ConditionManager.objects.create(owner=owner, community=owner.pk, set_on=set_on) setattr(target, attr_name, manager) target.save() condition_dict = self.condition_data if self.condition_data else {} data = {"condition_type": self.condition_type, "condition_data": condition_dict, "permission_data": self.permission_data} manager.add_condition(data_for_condition=data) manager.save() return manager