From 2b16bca262dd9e306b0b38c3960dd06d7a905d91 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Sep 2024 14:50:35 -0400 Subject: [PATCH 1/4] Closes #90: Implement branch archiving --- docs/models/branch.md | 1 + docs/using-branches/reverting-a-branch.md | 4 +- docs/using-branches/syncing-merging.md | 2 + netbox_branching/choices.py | 4 ++ netbox_branching/forms/misc.py | 8 +++ netbox_branching/models/branches.py | 14 +++++- .../templates/netbox_branching/branch.html | 9 ++++ .../netbox_branching/branch_archive.html | 43 ++++++++++++++++ netbox_branching/views.py | 50 +++++++++++++++++-- 9 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 netbox_branching/templates/netbox_branching/branch_archive.html diff --git a/docs/models/branch.md b/docs/models/branch.md index 560c87e..1ecbf59 100644 --- a/docs/models/branch.md +++ b/docs/models/branch.md @@ -29,6 +29,7 @@ The current status of the branch. This must be one of the following values. | Merging | A job is running to merge changes from the branch into main | | Reverting | A job is running to revert previously merged changes in main | | Merged | Changes from this branch have been successfully merged into main | +| Archived | A merged branch which has been deprovisioned in the database | | Failed | Provisioning the schema for this branch has failed | ### Last Sync diff --git a/docs/using-branches/reverting-a-branch.md b/docs/using-branches/reverting-a-branch.md index 5de33b5..68be470 100644 --- a/docs/using-branches/reverting-a-branch.md +++ b/docs/using-branches/reverting-a-branch.md @@ -2,8 +2,8 @@ Once a branch has been merged, it is generally no longer needed, and can no longer be activated. However, occasionally you may find it necessary to undo the changes from a branch (due to an error or an otherwise undesired state). This can be done by _reverting_ the branch. Only merged branches can be reverted. -!!! note - Only branches which have not yet been deleted can be reverted. Once a branch is deleted, reversion is no longer possible. +!!! warning + Only branches which have not yet been archived or deleted can be reverted. Once a branch's schema has been deprovisioned, it can no longer be reverted. Before reverting a branch, review the changes listed under its "Merged Changes" tab. NetBox will attempt to undo these specific changes when reverting the branch. diff --git a/docs/using-branches/syncing-merging.md b/docs/using-branches/syncing-merging.md index 27d5a55..af3c8f9 100644 --- a/docs/using-branches/syncing-merging.md +++ b/docs/using-branches/syncing-merging.md @@ -22,6 +22,8 @@ While a branch is being merged, its status will show "merging." !!! tip You can check on the status of the merging job under the "Jobs" tab of the branch view. +Once a branch has been merged, it can be [reverted](./reverting-a-branch.md), archived, or deleted. Archiving a branch removes its associated schema from the PostgreSQL database to deallocate space. An archived branch cannot be restored, however the branch record is retained for future reference. + ## Dealing with Conflicts In the event an object has been modified in both your branch _and_ in main in a diverging manner, this will be flagged as a conflict. For example, if both you and another user have modified the description of an interface to two different values in main and in the branch, this represents a conflict. diff --git a/netbox_branching/choices.py b/netbox_branching/choices.py index db8c9c8..6e49d2f 100644 --- a/netbox_branching/choices.py +++ b/netbox_branching/choices.py @@ -11,6 +11,7 @@ class BranchStatusChoices(ChoiceSet): MERGING = 'merging' REVERTING = 'reverting' MERGED = 'merged' + ARCHIVED = 'archived' FAILED = 'failed' CHOICES = ( @@ -21,6 +22,7 @@ class BranchStatusChoices(ChoiceSet): (MERGING, _('Merging'), 'orange'), (REVERTING, _('Reverting'), 'orange'), (MERGED, _('Merged'), 'blue'), + (ARCHIVED, _('Archived'), 'gray'), (FAILED, _('Failed'), 'red'), ) @@ -37,10 +39,12 @@ class BranchEventTypeChoices(ChoiceSet): SYNCED = 'synced' MERGED = 'merged' REVERTED = 'reverted' + ARCHIVED = 'archived' CHOICES = ( (PROVISIONED, _('Provisioned'), 'green'), (SYNCED, _('Synced'), 'cyan'), (MERGED, _('Merged'), 'blue'), (REVERTED, _('Reverted'), 'orange'), + (ARCHIVED, _('Archived'), 'gray'), ) diff --git a/netbox_branching/forms/misc.py b/netbox_branching/forms/misc.py index 2177f2f..ad895cb 100644 --- a/netbox_branching/forms/misc.py +++ b/netbox_branching/forms/misc.py @@ -5,6 +5,7 @@ __all__ = ( 'BranchActionForm', + 'ConfirmationForm', ) @@ -36,3 +37,10 @@ def clean(self): raise forms.ValidationError(_("All conflicts must be acknowledged in order to merge the branch.")) return self.cleaned_data + + +class ConfirmationForm(forms.Form): + confirm = forms.BooleanField( + required=True, + label=_('Confirm') + ) diff --git a/netbox_branching/models/branches.py b/netbox_branching/models/branches.py index b337ab1..140182a 100644 --- a/netbox_branching/models/branches.py +++ b/netbox_branching/models/branches.py @@ -417,7 +417,7 @@ def revert(self, user, commit=True): # Disconnect the signal receiver post_save.disconnect(handler, sender=ObjectChange_) - merge.alters_data = True + revert.alters_data = True def provision(self, user): """ @@ -512,6 +512,18 @@ def provision(self, user): provision.alters_data = True + def archive(self, user): + """ + Deprovision the Branch and set its status to "archived." + """ + self.deprovision() + + # Update the branch's status to "archived" + Branch.objects.filter(pk=self.pk).update(status=BranchStatusChoices.ARCHIVED) + BranchEvent.objects.create(branch=self, user=user, type=BranchEventTypeChoices.ARCHIVED) + + archive.alters_data = True + def deprovision(self): """ Delete the Branch's schema and all its tables from the database. diff --git a/netbox_branching/templates/netbox_branching/branch.html b/netbox_branching/templates/netbox_branching/branch.html index 5d8d6b1..75edc00 100644 --- a/netbox_branching/templates/netbox_branching/branch.html +++ b/netbox_branching/templates/netbox_branching/branch.html @@ -51,6 +51,15 @@ {% trans "Revert" %} {% endif %} + {% if perms.netbox_branching.archive_branch %} + + {% trans "Archive" %} + + {% else %} + + {% endif %} {% endif %} {% endblock %} diff --git a/netbox_branching/templates/netbox_branching/branch_archive.html b/netbox_branching/templates/netbox_branching/branch_archive.html new file mode 100644 index 0000000..8849acc --- /dev/null +++ b/netbox_branching/templates/netbox_branching/branch_archive.html @@ -0,0 +1,43 @@ +{% extends 'generic/_base.html' %} +{% load form_helpers %} +{% load i18n %} + +{% block title %}{% trans "Archive" %} {{ branch }}{% endblock %} + +{% block tabs %} + +{% endblock tabs %} + +{% block content %} + {# Form tab #} +
+
+ {% csrf_token %} +
+
+
+ + {% blocktrans %} + Are you sure you want to archive the branch {{ branch }}? This will permanently deprovision + its database schema, and it will no longer be possible to automatically rever the branch. + {% endblocktrans %} +
+ {% render_field form.confirm %} +
+ {% trans "Cancel" %} + +
+
+
+
+
+ {# /Form tab #} +{% endblock content %} diff --git a/netbox_branching/views.py b/netbox_branching/views.py index 7de7d16..ab99eea 100644 --- a/netbox_branching/views.py +++ b/netbox_branching/views.py @@ -12,7 +12,7 @@ from . import filtersets, forms, tables from .choices import BranchStatusChoices from .jobs import MergeBranchJob, RevertBranchJob, SyncBranchJob -from .models import ChangeDiff, Branch +from .models import Branch, ChangeDiff # @@ -186,7 +186,7 @@ def do_action(self, branch, request, form): def get(self, request, **kwargs): branch = self.get_object(**kwargs) - form = forms.BranchActionForm(branch) + form = self.form(branch) return render(request, self.template_name, { 'branch': branch, @@ -197,7 +197,7 @@ def get(self, request, **kwargs): def post(self, request, **kwargs): branch = self.get_object(**kwargs) - form = forms.BranchActionForm(branch, request.POST) + form = self.form(branch, request.POST) if branch.status not in self.valid_states: messages.error(request, _( @@ -265,6 +265,50 @@ def do_action(self, branch, request, form): return redirect(branch.get_absolute_url()) +@register_model_view(Branch, 'archive') +class BranchArchiveView(generic.ObjectView): + """ + Archive a merged Branch, deleting its database schema but retaining the Branch object. + """ + queryset = Branch.objects.all() + template_name = 'netbox_branching/branch_archive.html' + + def get_required_permission(self): + return f'netbox_branching.archive_branch' + + @staticmethod + def _enforce_status(request, branch): + if branch.status != BranchStatusChoices.MERGED: + messages.error(request, _("Only merged branches can be archived.")) + return redirect(branch.get_absolute_url()) + + def get(self, request, **kwargs): + branch = self.get_object(**kwargs) + self._enforce_status(request, branch) + form = forms.ConfirmationForm() + + return render(request, self.template_name, { + 'branch': branch, + 'form': form, + }) + + def post(self, request, **kwargs): + branch = self.get_object(**kwargs) + self._enforce_status(request, branch) + form = forms.ConfirmationForm(request.POST) + + if form.is_valid(): + branch.archive(user=request.user) + + messages.success(request, f"Branch {branch} has been archived.") + return redirect(branch.get_absolute_url()) + + return render(request, self.template_name, { + 'branch': branch, + 'form': form, + }) + + class BranchBulkImportView(generic.BulkImportView): queryset = Branch.objects.all() model_form = forms.BranchImportForm From e567ec74ecd3f73413c95562e260619aa2a8a6ee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Sep 2024 16:03:27 -0400 Subject: [PATCH 2/4] Exclude archived branch from nav dropdown --- netbox_branching/template_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox_branching/template_content.py b/netbox_branching/template_content.py index 2d2c154..83f8c68 100644 --- a/netbox_branching/template_content.py +++ b/netbox_branching/template_content.py @@ -17,7 +17,7 @@ class BranchSelector(PluginTemplateExtension): def navbar(self): return self.render('netbox_branching/inc/branch_selector.html', extra_context={ 'active_branch': active_branch.get(), - 'branches': Branch.objects.exclude(status=BranchStatusChoices.MERGED), + 'branches': Branch.objects.filter(status=BranchStatusChoices.READY), }) From 6d02f8d56605da2192fd81b4e2ce980f6c601945 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Sep 2024 16:04:29 -0400 Subject: [PATCH 3/4] Exclude archived branch from nav dropdown --- netbox_branching/template_content.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox_branching/template_content.py b/netbox_branching/template_content.py index 83f8c68..c0233d5 100644 --- a/netbox_branching/template_content.py +++ b/netbox_branching/template_content.py @@ -17,7 +17,9 @@ class BranchSelector(PluginTemplateExtension): def navbar(self): return self.render('netbox_branching/inc/branch_selector.html', extra_context={ 'active_branch': active_branch.get(), - 'branches': Branch.objects.filter(status=BranchStatusChoices.READY), + 'branches': Branch.objects.exclude( + status__in=[BranchStatusChoices.MERGED, BranchStatusChoices.ARCHIVED] + ), }) From 67f50367bb277a3d4f21c3fa6b05b232e00ff96c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Sep 2024 17:04:03 -0400 Subject: [PATCH 4/4] Fix translation support for branch action messages --- netbox_branching/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox_branching/views.py b/netbox_branching/views.py index ab99eea..38fb44c 100644 --- a/netbox_branching/views.py +++ b/netbox_branching/views.py @@ -225,7 +225,7 @@ def do_action(self, branch, request, form): user=request.user, commit=form.cleaned_data['commit'] ) - messages.success(request, f"Syncing of branch {branch} in progress") + messages.success(request, _("Syncing of branch {branch} in progress").format(branch=branch)) return redirect(branch.get_absolute_url()) @@ -241,7 +241,7 @@ def do_action(self, branch, request, form): user=request.user, commit=form.cleaned_data['commit'] ) - messages.success(request, f"Merging of branch {branch} in progress") + messages.success(request, _("Merging of branch {branch} in progress").format(branch=branch)) return redirect(branch.get_absolute_url()) @@ -260,7 +260,7 @@ def do_action(self, branch, request, form): user=request.user, commit=form.cleaned_data['commit'] ) - messages.success(request, f"Reverting branch {branch}") + messages.success(request, _("Reverting branch {branch}").format(branch=branch)) return redirect(branch.get_absolute_url()) @@ -300,7 +300,7 @@ def post(self, request, **kwargs): if form.is_valid(): branch.archive(user=request.user) - messages.success(request, f"Branch {branch} has been archived.") + messages.success(request, _("Branch {branch} has been archived.").format(branch=branch)) return redirect(branch.get_absolute_url()) return render(request, self.template_name, {