diff --git a/changes/+goldenconfig-bulk-edit.added b/changes/+goldenconfig-bulk-edit.added new file mode 100644 index 00000000..fd696a69 --- /dev/null +++ b/changes/+goldenconfig-bulk-edit.added @@ -0,0 +1 @@ +Added a bulk edit view for Golden Config (Configuration Overview) records. diff --git a/changes/+tags-support.added b/changes/+tags-support.added new file mode 100644 index 00000000..8c0cfc82 --- /dev/null +++ b/changes/+tags-support.added @@ -0,0 +1 @@ +Added tag support throughout the UI for all Golden Config models: tags can be added and removed in bulk edit, filtered on from every list view, and displayed as an optional table column. diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py index 654e6be8..466ea928 100644 --- a/nautobot_golden_config/forms.py +++ b/nautobot_golden_config/forms.py @@ -6,7 +6,7 @@ import django.forms as django_forms from django.conf import settings from nautobot.apps import forms -from nautobot.apps.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm +from nautobot.apps.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm, TagsBulkEditFormMixin from nautobot.dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Rack, RackGroup from nautobot.extras.models import DynamicGroup, GitRepository, GraphQLQuery, JobResult, Role, Status, Tag from nautobot.tenancy.models import Tenant, TenantGroup @@ -116,11 +116,13 @@ class GoldenConfigFilterForm(DeviceRelatedFilterForm): "device_status", "device_type", "device", + "tags", ] q = django_forms.CharField(required=False, label="Search") + tags = forms.TagFilterField(model) -class GoldenConfigBulkEditForm(NautobotBulkEditForm): +class GoldenConfigBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for GoldenConfig instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -154,9 +156,11 @@ class ConfigComplianceFilterForm(DeviceRelatedFilterForm): "device_status", "device_type", "device", + "tags", ] q = django_forms.CharField(required=False, label="Search") + tags = forms.TagFilterField(model) def __init__(self, *args, **kwargs): """Required for status to work.""" @@ -206,9 +210,10 @@ class ComplianceRuleFilterForm(NautobotFilterForm): ) feature = forms.DynamicModelMultipleChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) + tags = forms.TagFilterField(model) -class ComplianceRuleBulkEditForm(NautobotBulkEditForm): +class ComplianceRuleBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for ComplianceRule instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -250,6 +255,7 @@ class ComplianceFeatureFilterForm(NautobotFilterForm): model = models.ComplianceFeature q = django_forms.CharField(required=False, label="Search") name = forms.DynamicModelChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) + tags = forms.TagFilterField(model) class ComplianceFeatureFilterFormAlt(DeviceRelatedFilterForm): # pylint: disable=nb-sub-class-name @@ -289,7 +295,7 @@ def __init__(self, *args, **kwargs): self.order_fields(self.field_order) -class ComplianceFeatureBulkEditForm(NautobotBulkEditForm): +class ComplianceFeatureBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for ComplianceFeature instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -328,9 +334,10 @@ class ConfigRemoveFilterForm(NautobotFilterForm): name = forms.DynamicModelChoiceField( queryset=models.ConfigRemove.objects.all(), to_field_name="name", required=False ) + tags = forms.TagFilterField(model) -class ConfigRemoveBulkEditForm(NautobotBulkEditForm): +class ConfigRemoveBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for ConfigRemove instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -370,9 +377,10 @@ class ConfigReplaceFilterForm(NautobotFilterForm): name = forms.DynamicModelChoiceField( queryset=models.ConfigReplace.objects.all(), to_field_name="name", required=False ) + tags = forms.TagFilterField(model) -class ConfigReplaceBulkEditForm(NautobotBulkEditForm): +class ConfigReplaceBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for ConfigReplace instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -439,9 +447,10 @@ class GoldenConfigSettingFilterForm(NautobotFilterForm): queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.jinjatemplate"), required=False, ) + tags = forms.TagFilterField(model) -class GoldenConfigSettingBulkEditForm(NautobotBulkEditForm): +class GoldenConfigSettingBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for GoldenConfigSetting instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -479,9 +488,10 @@ class RemediationSettingFilterForm(NautobotFilterForm): widget=django_forms.Select(), label="Remediation Type", ) + tags = forms.TagFilterField(model) -class RemediationSettingBulkEditForm(NautobotBulkEditForm): +class RemediationSettingBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for RemediationSetting instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -640,7 +650,7 @@ class ConfigPlanFilterForm(DeviceRelatedFilterForm): tags = forms.TagFilterField(model) -class ConfigPlanBulkEditForm(NautobotBulkEditForm): +class ConfigPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """BulkEdit form for ConfigPlan instances.""" pk = django_forms.ModelMultipleChoiceField( @@ -660,7 +670,6 @@ class Meta: nullable_fields = [ "change_control_id", "change_control_url", - "tags", ] diff --git a/nautobot_golden_config/tables.py b/nautobot_golden_config/tables.py index 03be49b3..453c2774 100644 --- a/nautobot_golden_config/tables.py +++ b/nautobot_golden_config/tables.py @@ -109,7 +109,7 @@ MATCH_CONFIG = """{{ record.match_config|linebreaksbr }}""" -def actual_fields(): +def actual_fields(include_tags=False): """Convienance function to conditionally toggle columns.""" active_fields = ["pk", "name"] if ENABLE_BACKUP: @@ -118,6 +118,8 @@ def actual_fields(): active_fields.append("intended_last_success_date") if ENABLE_COMPLIANCE: active_fields.append("compliance_last_success_date") + if include_tags: + active_fields.append("tags") active_fields.append("actions") return tuple(active_fields) @@ -288,6 +290,8 @@ class GoldenConfigTable(BaseTable): order_by="compliance_last_success_date", ) + tags = TagColumn(url_name="plugins:nautobot_golden_config:goldenconfig_list") + actions = ButtonsColumn( buttons=("delete",), model=models.GoldenConfig, @@ -329,7 +333,8 @@ class Meta(BaseTable.Meta): """Meta for class GoldenConfigTable.""" model = models.GoldenConfig - fields = actual_fields() + fields = actual_fields(include_tags=True) + default_columns = actual_fields() # ComplianceFeature @@ -340,12 +345,13 @@ class ComplianceFeatureTable(BaseTable): pk = ToggleColumn() name = LinkColumn("plugins:nautobot_golden_config:compliancefeature", args=[A("pk")]) + tags = TagColumn(url_name="plugins:nautobot_golden_config:compliancefeature_list") class Meta(BaseTable.Meta): """Table to display Compliance Features Meta Data.""" model = models.ComplianceFeature - fields = ("pk", "name", "slug", "description") + fields = ("pk", "name", "slug", "description", "tags") default_columns = ("pk", "name", "slug", "description") @@ -361,6 +367,7 @@ class ComplianceRuleTable(BaseTable): config_ordered = BooleanColumn() custom_compliance = BooleanColumn() config_remediation = BooleanColumn() + tags = TagColumn(url_name="plugins:nautobot_golden_config:compliancerule_list") class Meta(BaseTable.Meta): """Table to display Compliance Rules Meta Data.""" @@ -376,6 +383,7 @@ class Meta(BaseTable.Meta): "config_type", "custom_compliance", "config_remediation", + "tags", ) default_columns = ( "pk", @@ -398,12 +406,13 @@ class ConfigRemoveTable(BaseTable): pk = ToggleColumn() name = LinkColumn("plugins:nautobot_golden_config:configremove", args=[A("pk")]) + tags = TagColumn(url_name="plugins:nautobot_golden_config:configremove_list") class Meta(BaseTable.Meta): """Table to display Compliance Rules Meta Data.""" model = models.ConfigRemove - fields = ("pk", "name", "platform", "description", "regex") + fields = ("pk", "name", "platform", "description", "regex", "tags") default_columns = ("pk", "name", "platform", "description", "regex") @@ -415,12 +424,13 @@ class ConfigReplaceTable(BaseTable): pk = ToggleColumn() name = LinkColumn("plugins:nautobot_golden_config:configreplace", args=[A("pk")]) + tags = TagColumn(url_name="plugins:nautobot_golden_config:configreplace_list") class Meta(BaseTable.Meta): """Table to display Compliance Rules Meta Data.""" model = models.ConfigReplace - fields = ("pk", "name", "platform", "description", "regex", "replace") + fields = ("pk", "name", "platform", "description", "regex", "replace", "tags") default_columns = ("pk", "name", "platform", "description", "regex", "replace") @@ -442,6 +452,7 @@ class GoldenConfigSettingTable(BaseTable): verbose_name="Backup Repository", empty_values=(), ) + tags = TagColumn(url_name="plugins:nautobot_golden_config:goldenconfigsetting_list") def _render_capability(self, record, column, record_attribute): # pylint: disable=unused-argument if getattr(record, record_attribute, None): @@ -472,6 +483,16 @@ class Meta(BaseTable.Meta): "backup_repository", "intended_repository", "jinja_repository", + "tags", + ) + default_columns = ( + "pk", + "name", + "weight", + "description", + "backup_repository", + "intended_repository", + "jinja_repository", ) @@ -480,12 +501,13 @@ class RemediationSettingTable(BaseTable): pk = ToggleColumn() platform = LinkColumn("plugins:nautobot_golden_config:remediationsetting", args=[A("pk")]) + tags = TagColumn(url_name="plugins:nautobot_golden_config:remediationsetting_list") class Meta(BaseTable.Meta): """Table to display RemediationSetting Meta Data.""" model = models.RemediationSetting - fields = ("pk", "platform", "remediation_type") + fields = ("pk", "platform", "remediation_type", "tags") default_columns = ("pk", "platform", "remediation_type") diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py index c67412ae..17bfb94c 100644 --- a/nautobot_golden_config/views.py +++ b/nautobot_golden_config/views.py @@ -60,6 +60,7 @@ class GoldenConfigUIViewSet( # pylint: disable=abstract-method views.ObjectDetailViewMixin, views.ObjectDestroyViewMixin, views.ObjectBulkDestroyViewMixin, + views.ObjectBulkUpdateViewMixin, views.ObjectListViewMixin, # TODO: Changing the order of the mixins breaks things... why? ObjectDataComplianceViewMixin, # TODO: Import from views after nautobot release ):