diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml
index 6c594b7..1bff827 100644
--- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml
@@ -1,6 +1,6 @@
---
name: ✨ Feature Request
-description: Propose a new NetBox feature or enhancement
+description: Propose a new feature or enhancement
labels: ["type: feature"]
body:
- type: input
@@ -13,21 +13,21 @@ body:
attributes:
label: Proposed functionality
description: >
- Describe in detail the new feature or behavior you are proposing. Include any specific changes
- to work flows, data models, and/or the user interface. The more detail you provide here, the
- greater chance your proposal has of being discussed.
+ Describe in detail the new feature or behavior you are proposing. Include any specific changes to work flows,
+ data models, and/or the user interface. The more detail you provide here, the greater chance your proposal has
+ of being discussed.
validations:
required: true
- type: textarea
attributes:
label: Use case
description: >
- Explain how adding this functionality would benefit users. What need does it address?
+ Explain how adding this functionality would benefit users. What specific need(s) does it address?
validations:
required: true
- type: textarea
attributes:
label: External dependencies
description: >
- List any new dependencies on external libraries or services that this new feature would
- introduce. For example, does the proposal require the installation of a new Python package?
+ List any new dependencies on external libraries or services that this new feature would introduce. For example,
+ does the proposal require the installation of a new Python package?
diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml
index a085f62..7117388 100644
--- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml
@@ -25,9 +25,8 @@ body:
attributes:
label: Steps to Reproduce
description: >
- Describe in detail the exact steps that someone else can take to reproduce
- this bug. A numbered list of discrete steps is strongly preferred. Remember
- to capture the creation of any objects which must exist to reproduce the
+ Describe in detail the exact steps that someone else can take to reproduce this bug. A numbered list of discrete
+ steps is strongly preferred. Remember to capture the creation of any objects which must exist to reproduce the
behavior.
placeholder: |
1. Click on "create widget"
@@ -45,7 +44,7 @@ body:
- type: textarea
attributes:
label: Observed Behavior
- description: What happened instead?
+ description: What happened instead? Be sure to include any error messages.
placeholder: A TypeError exception was raised
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/03-documentation_change.yaml b/.github/ISSUE_TEMPLATE/03-documentation_change.yaml
index 9f7968e..f9489e5 100644
--- a/.github/ISSUE_TEMPLATE/03-documentation_change.yaml
+++ b/.github/ISSUE_TEMPLATE/03-documentation_change.yaml
@@ -18,6 +18,6 @@ body:
- type: textarea
attributes:
label: Proposed Changes
- description: Describe the proposed changes and why they are necessary.
+ description: Describe the proposed changes and explain why they are necessary.
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/04-housekeeping.yaml b/.github/ISSUE_TEMPLATE/04-housekeeping.yaml
index cec1500..c8b3de5 100644
--- a/.github/ISSUE_TEMPLATE/04-housekeeping.yaml
+++ b/.github/ISSUE_TEMPLATE/04-housekeeping.yaml
@@ -1,6 +1,6 @@
---
name: 🏡 Housekeeping
-description: A change pertaining to the codebase itself (developers only)
+description: An internal change pertaining to the codebase itself
labels: ["type: housekeeping"]
body:
- type: textarea
diff --git a/docs/changelog.md b/docs/changelog.md
index 6594aeb..9652c79 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,19 @@
# Change Log
+## v0.5.3
+
+### Enhancements
+
+* [#209](https://github.com/netboxlabs/netbox-branching/issues/209) - Prevent merging branches whose `last_sync` time exceeds the configured changelog retention window
+
+### Bug Fixes
+
+* [#87](https://github.com/netboxlabs/netbox-branching/issues/87) - Deactivate the active branch (if any) when creating a new branch
+* [#148](https://github.com/netboxlabs/netbox-branching/issues/148) - Fix `IntegrityError` exception raised when executing custom scripts within a branch
+* [#178](https://github.com/netboxlabs/netbox-branching/issues/178) - Fix display of assigned tags in the branches list
+
+---
+
## v0.5.2
### Bug Fixes
diff --git a/docs/using-branches/syncing-merging.md b/docs/using-branches/syncing-merging.md
index af3c8f9..3c7d0cd 100644
--- a/docs/using-branches/syncing-merging.md
+++ b/docs/using-branches/syncing-merging.md
@@ -6,6 +6,9 @@ Synchronizing a branch replicates all recent changes from main into the branch.
To synchronize a branch, click the "Sync" button. (If this button is not visible, verify that the branch status shows "ready" and that you have permission to synchronize the branch.)
+!!! warning
+ A branch must be synchronized frequently enough to avoid exceeding NetBox's configured [changelog retention period](https://netboxlabs.com/docs/netbox/en/stable/configuration/miscellaneous/#changelog_retention) (which defaults to 90 days). This is to protect against data loss when replicating changes from main. A branch whose `last_sync` time exceeds the configured retention window can no longer be synced.
+
While a branch is being synchronized, its status will show "synchronizing."
!!! tip
diff --git a/netbox_branching/__init__.py b/netbox_branching/__init__.py
index 7ba11ca..26c07ba 100644
--- a/netbox_branching/__init__.py
+++ b/netbox_branching/__init__.py
@@ -9,9 +9,9 @@ class AppConfig(PluginConfig):
name = 'netbox_branching'
verbose_name = 'NetBox Branching'
description = 'A git-like branching implementation for NetBox'
- version = '0.5.2'
+ version = '0.5.3'
base_url = 'branching'
- min_version = '4.1'
+ min_version = '4.1.9'
middleware = [
'netbox_branching.middleware.BranchMiddleware'
]
diff --git a/netbox_branching/middleware.py b/netbox_branching/middleware.py
index a79e994..c339365 100644
--- a/netbox_branching/middleware.py
+++ b/netbox_branching/middleware.py
@@ -1,14 +1,8 @@
-from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseBadRequest
-from django.urls import reverse
-from utilities.api import is_api_request
-
-from .choices import BranchStatusChoices
-from .constants import COOKIE_NAME, BRANCH_HEADER, QUERY_PARAM
-from .models import Branch
-from .utilities import activate_branch, is_api_request
+from .constants import COOKIE_NAME, QUERY_PARAM
+from .utilities import activate_branch, is_api_request, get_active_branch
__all__ = (
'BranchMiddleware',
@@ -24,12 +18,11 @@ def __call__(self, request):
# Set/clear the active Branch on the request
try:
- branch = self.get_active_branch(request)
+ branch = get_active_branch(request)
except ObjectDoesNotExist:
return HttpResponseBadRequest("Invalid branch identifier")
- with activate_branch(branch):
- response = self.get_response(request)
+ response = self.get_response(request)
# Set/clear the branch cookie (for non-API requests)
if not is_api_request(request):
@@ -39,34 +32,3 @@ def __call__(self, request):
response.delete_cookie(COOKIE_NAME)
return response
-
- @staticmethod
- def get_active_branch(request):
- """
- Return the active Branch (if any).
- """
- # The active Branch may be specified by HTTP header for REST & GraphQL API requests.
- if is_api_request(request) and BRANCH_HEADER in request.headers:
- branch = Branch.objects.get(schema_id=request.headers.get(BRANCH_HEADER))
- if not branch.ready:
- return HttpResponseBadRequest(f"Branch {branch} is not ready for use (status: {branch.status})")
- return branch
-
- # Branch activated/deactivated by URL query parameter
- elif QUERY_PARAM in request.GET:
- if schema_id := request.GET.get(QUERY_PARAM):
- branch = Branch.objects.get(schema_id=schema_id)
- if branch.ready:
- messages.success(request, f"Activated branch {branch}")
- return branch
- else:
- messages.error(request, f"Branch {branch} is not ready for use (status: {branch.status})")
- return None
- else:
- messages.success(request, f"Deactivated branch")
- request.COOKIES.pop(COOKIE_NAME, None) # Delete cookie if set
- return None
-
- # Branch set by cookie
- elif schema_id := request.COOKIES.get(COOKIE_NAME):
- return Branch.objects.filter(schema_id=schema_id, status=BranchStatusChoices.READY).first()
diff --git a/netbox_branching/models/branches.py b/netbox_branching/models/branches.py
index be5712d..6ace090 100644
--- a/netbox_branching/models/branches.py
+++ b/netbox_branching/models/branches.py
@@ -1,6 +1,7 @@
import logging
import random
import string
+from datetime import timedelta
from functools import cached_property, partial
from django.conf import settings
@@ -15,6 +16,7 @@
from django.utils.translation import gettext_lazy as _
from core.models import ObjectChange as ObjectChange_
+from netbox.config import get_config
from netbox.context import current_request
from netbox.context_managers import event_tracking
from netbox.models import PrimaryModel
@@ -121,7 +123,7 @@ def schema_name(self):
def connection_name(self):
return f'schema_{self.schema_name}'
- @cached_property
+ @property
def synced_time(self):
return self.last_sync or self.created
@@ -159,9 +161,6 @@ def save(self, provision=True, *args, **kwargs):
_provision = provision and self.pk is None
- if active_branch.get():
- raise AbortRequest(_("Cannot create or modify a branch while a branch is active."))
-
super().save(*args, **kwargs)
if _provision:
@@ -173,8 +172,8 @@ def save(self, provision=True, *args, **kwargs):
)
def delete(self, *args, **kwargs):
- if active_branch.get():
- raise AbortRequest(_("Cannot delete a branch while a branch is active."))
+ if active_branch.get() == self:
+ raise AbortRequest(_("The active branch cannot be deleted."))
# Deprovision the schema
self.deprovision()
@@ -243,6 +242,16 @@ def get_event_history(self):
last_time = event.time
return history
+ @property
+ def is_stale(self):
+ """
+ Indicates whether the branch is too far out of date to be synced.
+ """
+ if not (changelog_retention := get_config().CHANGELOG_RETENTION):
+ # Changelog retention is disabled
+ return False
+ return self.synced_time < timezone.now() - timedelta(days=changelog_retention)
+
def sync(self, user, commit=True):
"""
Apply changes from the main schema onto the Branch's schema.
@@ -252,6 +261,8 @@ def sync(self, user, commit=True):
if not self.ready:
raise Exception(f"Branch {self} is not ready to sync")
+ if self.is_stale:
+ raise Exception(f"Branch {self} is stale and can no longer be synced")
# Emit pre-sync signal
pre_sync.send(sender=self.__class__, branch=self, user=user)
diff --git a/netbox_branching/navigation.py b/netbox_branching/navigation.py
index c773814..fa48de5 100644
--- a/netbox_branching/navigation.py
+++ b/netbox_branching/navigation.py
@@ -11,7 +11,7 @@
link_text=_('Branches'),
buttons=(
PluginMenuButton('plugins:netbox_branching:branch_add', _('Add'), 'mdi mdi-plus-thick'),
- PluginMenuButton('plugins:netbox_branching:branch_import', _('Import'), 'mdi mdi-upload'),
+ PluginMenuButton('plugins:netbox_branching:branch_bulk_import', _('Import'), 'mdi mdi-upload'),
)
),
PluginMenuItem(
diff --git a/netbox_branching/tables/tables.py b/netbox_branching/tables/tables.py
index f1df9e6..8d90a59 100644
--- a/netbox_branching/tables/tables.py
+++ b/netbox_branching/tables/tables.py
@@ -56,8 +56,16 @@ class BranchTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
+ is_active = columns.BooleanColumn(
+ verbose_name=_('Active')
+ )
status = columns.ChoiceFieldColumn(
- verbose_name=_('Status'),
+ verbose_name=_('Status')
+ )
+ is_stale = columns.BooleanColumn(
+ true_mark=mark_safe(''),
+ false_mark=None,
+ verbose_name=_('Stale')
)
conflicts = ConflictsColumn(
verbose_name=_('Conflicts')
@@ -65,15 +73,18 @@ class BranchTable(NetBoxTable):
schema_id = tables.TemplateColumn(
template_code='{{ value }}'
)
+ tags = columns.TagColumn(
+ url_name='plugins:netbox_branching:branch_list'
+ )
class Meta(NetBoxTable.Meta):
model = Branch
fields = (
- 'pk', 'id', 'name', 'is_active', 'status', 'conflicts', 'schema_id', 'description', 'owner', 'tags',
- 'created', 'last_updated',
+ 'pk', 'id', 'name', 'is_active', 'status', 'is_stale', 'conflicts', 'schema_id', 'description', 'owner',
+ 'tags', 'created', 'last_updated',
)
default_columns = (
- 'pk', 'name', 'is_active', 'status', 'owner', 'conflicts', 'schema_id', 'description',
+ 'pk', 'name', 'is_active', 'status', 'is_stale', 'owner', 'conflicts', 'schema_id', 'description',
)
def render_is_active(self, value):
diff --git a/netbox_branching/template_content.py b/netbox_branching/template_content.py
index 063a9df..b4993a0 100644
--- a/netbox_branching/template_content.py
+++ b/netbox_branching/template_content.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
-from netbox.plugins import PluginTemplateExtension
+from netbox.plugins import PluginTemplateExtension
from .choices import BranchStatusChoices
from .contextvars import active_branch
from .models import Branch, ChangeDiff
@@ -8,6 +8,8 @@
__all__ = (
'BranchNotification',
'BranchSelector',
+ 'ScriptNotification',
+ 'ShareButton',
'template_extensions',
)
@@ -34,6 +36,7 @@ class BranchNotification(PluginTemplateExtension):
def alerts(self):
if not (instance := self.context['object']):
return ''
+
ct = ContentType.objects.get_for_model(instance)
relevant_changes = ChangeDiff.objects.filter(
object_type=ct,
@@ -51,4 +54,18 @@ def alerts(self):
})
-template_extensions = [BranchSelector, ShareButton, BranchNotification]
+class ScriptNotification(PluginTemplateExtension):
+ models = ['extras.script']
+
+ def alerts(self):
+ return self.render('netbox_branching/inc/script_alert.html', extra_context={
+ 'active_branch': active_branch.get(),
+ })
+
+
+template_extensions = (
+ BranchSelector,
+ BranchNotification,
+ ScriptNotification,
+ ShareButton,
+)
diff --git a/netbox_branching/templates/netbox_branching/branch.html b/netbox_branching/templates/netbox_branching/branch.html
index 75eff0c..08705d8 100644
--- a/netbox_branching/templates/netbox_branching/branch.html
+++ b/netbox_branching/templates/netbox_branching/branch.html
@@ -82,11 +82,24 @@