diff --git a/README.md b/README.md
index 350c238..5196850 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ PLUGINS = [
]
```
-5. Create `local_settings.py` to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support.
+5. Create `local_settings.py` (in the same directory as `settings.py`) to override the `DATABASES` & `DATABASE_ROUTERS` settings. This enables dynamic schema support.
```python
from netbox_branching.utilities import DynamicSchemaDict
diff --git a/docs/changelog.md b/docs/changelog.md
index a384c0a..4d79fed 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,23 +1,41 @@
# Change Log
+## v0.5.1
+
+### Enhancements
+
+* [#123](https://github.com/netboxlabs/netbox-branching/issues/123) - Introduce template tags for branch action buttons
+* [#129](https://github.com/netboxlabs/netbox-branching/issues/129) - Implement pre-event signals for branch actions
+
+### Bug Fixes
+
+* [#98](https://github.com/netboxlabs/netbox-branching/issues/98) - Cable changes in branch should not impact main schema
+* [#119](https://github.com/netboxlabs/netbox-branching/issues/119) - Fix the dynamic selection of related objects in forms while a branch is active
+* [#120](https://github.com/netboxlabs/netbox-branching/issues/120) - `max_branches` config parameter should disregard archived branches
+* [#138](https://github.com/netboxlabs/netbox-branching/issues/138) - Fix rendering the ID column of the change diffs table
+* [#140](https://github.com/netboxlabs/netbox-branching/issues/140) - Fix representation of branch status in REST API
+* [#142](https://github.com/netboxlabs/netbox-branching/issues/142) - Fix tab record counts for archived branches
+
+---
+
## v0.5.0
### Enhancements
-* [#83](https://github.com/netboxlabs/nbl-netbox-branching/issues/83) - Add a "share" button under object views when a branch is active
-* [#84](https://github.com/netboxlabs/nbl-netbox-branching/issues/84) - Introduce the `max_working_branches` configuration parameter
-* [#88](https://github.com/netboxlabs/nbl-netbox-branching/issues/88) - Add branching support for NetBox's graphQL API
-* [#90](https://github.com/netboxlabs/nbl-netbox-branching/issues/90) - Introduce the ability to archive & deprovision merged branches without deleting them
-* [#97](https://github.com/netboxlabs/nbl-netbox-branching/issues/97) - Introduce the `exempt_models` config parameter to disable branching support for plugin models
-* [#116](https://github.com/netboxlabs/nbl-netbox-branching/issues/116) - Disable branching support for applicable core models
+* [#83](https://github.com/netboxlabs/netbox-branching/issues/83) - Add a "share" button under object views when a branch is active
+* [#84](https://github.com/netboxlabs/netbox-branching/issues/84) - Introduce the `max_working_branches` configuration parameter
+* [#88](https://github.com/netboxlabs/netbox-branching/issues/88) - Add branching support for NetBox's graphQL API
+* [#90](https://github.com/netboxlabs/netbox-branching/issues/90) - Introduce the ability to archive & deprovision merged branches without deleting them
+* [#97](https://github.com/netboxlabs/netbox-branching/issues/97) - Introduce the `exempt_models` config parameter to disable branching support for plugin models
+* [#116](https://github.com/netboxlabs/netbox-branching/issues/116) - Disable branching support for applicable core models
### Bug Fixes
-* [#81](https://github.com/netboxlabs/nbl-netbox-branching/issues/81) - Fix event rule triggering for the `branch_reverted` event
-* [#91](https://github.com/netboxlabs/nbl-netbox-branching/issues/91) - Disregard the active branch (if any) when alerting on changes under object views
-* [#94](https://github.com/netboxlabs/nbl-netbox-branching/issues/94) - Fix branch merging after modifying an object with custom field data
-* [#101](https://github.com/netboxlabs/nbl-netbox-branching/issues/101) - Permit (but warn about) database queries issued before branching support has been initialized
-* [#102](https://github.com/netboxlabs/nbl-netbox-branching/issues/102) - Record individual object actions in branch job logs
+* [#81](https://github.com/netboxlabs/netbox-branching/issues/81) - Fix event rule triggering for the `branch_reverted` event
+* [#91](https://github.com/netboxlabs/netbox-branching/issues/91) - Disregard the active branch (if any) when alerting on changes under object views
+* [#94](https://github.com/netboxlabs/netbox-branching/issues/94) - Fix branch merging after modifying an object with custom field data
+* [#101](https://github.com/netboxlabs/netbox-branching/issues/101) - Permit (but warn about) database queries issued before branching support has been initialized
+* [#102](https://github.com/netboxlabs/netbox-branching/issues/102) - Record individual object actions in branch job logs
---
@@ -25,18 +43,18 @@
### Enhancements
-* [#52](https://github.com/netboxlabs/nbl-netbox-branching/issues/52) - Introduce the `max_branches` config parameter
-* [#71](https://github.com/netboxlabs/nbl-netbox-branching/issues/71) - Ensure the consistent application of logging messages
-* [#76](https://github.com/netboxlabs/nbl-netbox-branching/issues/76) - Validate required configuration items on initialization
+* [#52](https://github.com/netboxlabs/netbox-branching/issues/52) - Introduce the `max_branches` config parameter
+* [#71](https://github.com/netboxlabs/netbox-branching/issues/71) - Ensure the consistent application of logging messages
+* [#76](https://github.com/netboxlabs/netbox-branching/issues/76) - Validate required configuration items on initialization
### Bug Fixes
-* [#57](https://github.com/netboxlabs/nbl-netbox-branching/issues/57) - Avoid recording ChangeDiff records for unsupported object types
-* [#59](https://github.com/netboxlabs/nbl-netbox-branching/issues/59) - `BranchAwareRouter` should consider branching support for model when determining database connection to use
-* [#61](https://github.com/netboxlabs/nbl-netbox-branching/issues/61) - Fix transaction rollback when performing a dry run sync
-* [#66](https://github.com/netboxlabs/nbl-netbox-branching/issues/66) - Capture object representation on ChangeDiff when creating a new object within a branch
-* [#69](https://github.com/netboxlabs/nbl-netbox-branching/issues/69) - Represent null values for ChangeDiff fields consistently in REST API
-* [#73](https://github.com/netboxlabs/nbl-netbox-branching/issues/73) - Ensure all relevant branch diffs are updated when an object is modified in main
+* [#57](https://github.com/netboxlabs/netbox-branching/issues/57) - Avoid recording ChangeDiff records for unsupported object types
+* [#59](https://github.com/netboxlabs/netbox-branching/issues/59) - `BranchAwareRouter` should consider branching support for model when determining database connection to use
+* [#61](https://github.com/netboxlabs/netbox-branching/issues/61) - Fix transaction rollback when performing a dry run sync
+* [#66](https://github.com/netboxlabs/netbox-branching/issues/66) - Capture object representation on ChangeDiff when creating a new object within a branch
+* [#69](https://github.com/netboxlabs/netbox-branching/issues/69) - Represent null values for ChangeDiff fields consistently in REST API
+* [#73](https://github.com/netboxlabs/netbox-branching/issues/73) - Ensure all relevant branch diffs are updated when an object is modified in main
---
@@ -44,10 +62,10 @@
### Bug Fixes
-* [#42](https://github.com/netboxlabs/nbl-netbox-branching/issues/42) - Fix exception raised when viewing custom scripts
-* [#44](https://github.com/netboxlabs/nbl-netbox-branching/issues/44) - Handle truncated SQL sequence names to avoid exceptions during branch provisioning
-* [#48](https://github.com/netboxlabs/nbl-netbox-branching/issues/48) - Ensure background job is terminated in the event branch provisioning errors
-* [#50](https://github.com/netboxlabs/nbl-netbox-branching/issues/50) - Branch state should remain as "merged" after dry-run revert
+* [#42](https://github.com/netboxlabs/netbox-branching/issues/42) - Fix exception raised when viewing custom scripts
+* [#44](https://github.com/netboxlabs/netbox-branching/issues/44) - Handle truncated SQL sequence names to avoid exceptions during branch provisioning
+* [#48](https://github.com/netboxlabs/netbox-branching/issues/48) - Ensure background job is terminated in the event branch provisioning errors
+* [#50](https://github.com/netboxlabs/netbox-branching/issues/50) - Branch state should remain as "merged" after dry-run revert
---
@@ -55,23 +73,23 @@
### Enhancements
-* [#2](https://github.com/netboxlabs/nbl-netbox-branching/issues/2) - Enable the ability to revert a previously merged branch
-* [#3](https://github.com/netboxlabs/nbl-netbox-branching/issues/3) - Require review & acknowledgment of conflicts before syncing or merging a branch
-* [#4](https://github.com/netboxlabs/nbl-netbox-branching/issues/4) - Include a three-way diff summary in the REST API representation of a modified object
-* [#13](https://github.com/netboxlabs/nbl-netbox-branching/issues/13) - Add a link to the active branch in the branch selector dropdown
-* [#15](https://github.com/netboxlabs/nbl-netbox-branching/issues/15) - Default to performing a "dry run" for branch sync & merge
-* [#17](https://github.com/netboxlabs/nbl-netbox-branching/issues/17) - Utilize NetBox's `JobRunner` class for background jobs
-* [#29](https://github.com/netboxlabs/nbl-netbox-branching/issues/29) - Register a branch column on NetBox's global changelog table
-* [#36](https://github.com/netboxlabs/nbl-netbox-branching/issues/36) - Run the branch provisioning process within an isolated transaction
+* [#2](https://github.com/netboxlabs/netbox-branching/issues/2) - Enable the ability to revert a previously merged branch
+* [#3](https://github.com/netboxlabs/netbox-branching/issues/3) - Require review & acknowledgment of conflicts before syncing or merging a branch
+* [#4](https://github.com/netboxlabs/netbox-branching/issues/4) - Include a three-way diff summary in the REST API representation of a modified object
+* [#13](https://github.com/netboxlabs/netbox-branching/issues/13) - Add a link to the active branch in the branch selector dropdown
+* [#15](https://github.com/netboxlabs/netbox-branching/issues/15) - Default to performing a "dry run" for branch sync & merge
+* [#17](https://github.com/netboxlabs/netbox-branching/issues/17) - Utilize NetBox's `JobRunner` class for background jobs
+* [#29](https://github.com/netboxlabs/netbox-branching/issues/29) - Register a branch column on NetBox's global changelog table
+* [#36](https://github.com/netboxlabs/netbox-branching/issues/36) - Run the branch provisioning process within an isolated transaction
### Bug Fixes
-* [#10](https://github.com/netboxlabs/nbl-netbox-branching/issues/10) - Fix branch merge failure when deleted object was modified in another branch
-* [#11](https://github.com/netboxlabs/nbl-netbox-branching/issues/11) - Fix quick search functionality for branch diffs tab
-* [#16](https://github.com/netboxlabs/nbl-netbox-branching/issues/16) - Fix support for many-to-many assignments
-* [#24](https://github.com/netboxlabs/nbl-netbox-branching/issues/24) - Correct the REST API schema for the sync, merge, and revert branch endpoints
-* [#30](https://github.com/netboxlabs/nbl-netbox-branching/issues/30) - Include only unmerged branches with relevant changes in object view notifications
-* [#31](https://github.com/netboxlabs/nbl-netbox-branching/issues/31) - Prevent the deletion of a branch in a transitional state
+* [#10](https://github.com/netboxlabs/netbox-branching/issues/10) - Fix branch merge failure when deleted object was modified in another branch
+* [#11](https://github.com/netboxlabs/netbox-branching/issues/11) - Fix quick search functionality for branch diffs tab
+* [#16](https://github.com/netboxlabs/netbox-branching/issues/16) - Fix support for many-to-many assignments
+* [#24](https://github.com/netboxlabs/netbox-branching/issues/24) - Correct the REST API schema for the sync, merge, and revert branch endpoints
+* [#30](https://github.com/netboxlabs/netbox-branching/issues/30) - Include only unmerged branches with relevant changes in object view notifications
+* [#31](https://github.com/netboxlabs/netbox-branching/issues/31) - Prevent the deletion of a branch in a transitional state
---
diff --git a/docs/index.md b/docs/index.md
index 162404f..200c688 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -124,7 +124,7 @@ PLUGINS = [
This plugin employs dynamic schema resolution, which requires that we override two low-level Django settings. First, we'll wrap NetBox's configured `DATABASE` parameter with `DynamicSchemaDict` to support dynamic schemas. Second, we'll employ the plugin's custom database router.
-Create a new file named `local_settings.py` in the same directory as `configuration.py`, and add the content below.
+Create a new file named `local_settings.py` in the same directory as `settings.py`, and add the content below.
```python
from netbox_branching.utilities import DynamicSchemaDict
diff --git a/netbox_branching/__init__.py b/netbox_branching/__init__.py
index de903be..2d3054c 100644
--- a/netbox_branching/__init__.py
+++ b/netbox_branching/__init__.py
@@ -9,7 +9,7 @@ class AppConfig(PluginConfig):
name = 'netbox_branching'
verbose_name = 'NetBox Branching'
description = 'A git-like branching implementation for NetBox'
- version = '0.5.0'
+ version = '0.5.1'
base_url = 'branching'
min_version = '4.1'
middleware = [
diff --git a/netbox_branching/api/serializers.py b/netbox_branching/api/serializers.py
index 2336f7c..d7b24c6 100644
--- a/netbox_branching/api/serializers.py
+++ b/netbox_branching/api/serializers.py
@@ -5,7 +5,7 @@
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
-from netbox_branching.choices import BranchEventTypeChoices
+from netbox_branching.choices import BranchEventTypeChoices, BranchStatusChoices
from netbox_branching.models import ChangeDiff, Branch, BranchEvent
from users.api.serializers import UserSerializer
from utilities.api import get_serializer_for_model
@@ -30,6 +30,9 @@ class BranchSerializer(NetBoxModelSerializer):
nested=True,
read_only=True
)
+ status = ChoiceField(
+ choices=BranchStatusChoices
+ )
class Meta:
model = Branch
diff --git a/netbox_branching/constants.py b/netbox_branching/constants.py
index ea9e7fa..938963d 100644
--- a/netbox_branching/constants.py
+++ b/netbox_branching/constants.py
@@ -10,6 +10,12 @@
# URL query parameter name
QUERY_PARAM = '_branch'
+# Tables which must be replicated within a branch even though their
+# models don't directly support branching.
+REPLICATE_TABLES = (
+ 'dcim_cablepath',
+)
+
# Models for which branching support is explicitly disabled
EXEMPT_MODELS = (
# Exempt applicable core NetBox models
diff --git a/netbox_branching/database.py b/netbox_branching/database.py
index 842f881..3c749fc 100644
--- a/netbox_branching/database.py
+++ b/netbox_branching/database.py
@@ -21,11 +21,6 @@ def _get_db(self, model, **hints):
warnings.warn(f"Routing database query for {model} before branching support is initialized.")
return
- # Bail if the model does not support branching
- app_label, model_name = model._meta.label.lower().split('.')
- if model_name not in registry['model_features']['branching'].get(app_label, []):
- return
-
# Return the schema for the active branch (if any)
if branch := active_branch.get():
return f'schema_{branch.schema_name}'
diff --git a/netbox_branching/middleware.py b/netbox_branching/middleware.py
index 79f9bbc..a79e994 100644
--- a/netbox_branching/middleware.py
+++ b/netbox_branching/middleware.py
@@ -8,7 +8,7 @@
from .choices import BranchStatusChoices
from .constants import COOKIE_NAME, BRANCH_HEADER, QUERY_PARAM
from .models import Branch
-from .utilities import activate_branch
+from .utilities import activate_branch, is_api_request
__all__ = (
'BranchMiddleware',
@@ -45,13 +45,12 @@ def get_active_branch(request):
"""
Return the active Branch (if any).
"""
- # The active Branch is specified by HTTP header for REST & GraphQL API requests.
- if request.path_info.startswith(reverse('api-root')) or request.path_info.startswith(reverse('graphql')):
- if schema_id := request.headers.get(BRANCH_HEADER):
- branch = Branch.objects.get(schema_id=schema_id)
- if not branch.ready:
- return HttpResponseBadRequest(f"Branch {branch} is not ready for use (status: {branch.status})")
- return branch
+ # 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:
diff --git a/netbox_branching/models/branches.py b/netbox_branching/models/branches.py
index 23591d0..be5712d 100644
--- a/netbox_branching/models/branches.py
+++ b/netbox_branching/models/branches.py
@@ -129,12 +129,12 @@ def clean(self):
# Enforce the maximum number of total branches
if not self.pk and (max_branches := get_plugin_config('netbox_branching', 'max_branches')):
- total_branch_count = Branch.objects.count()
+ total_branch_count = Branch.objects.exclude(status=BranchStatusChoices.ARCHIVED).count()
if total_branch_count >= max_branches:
raise ValidationError(
_(
- "The configured maximum number of branches ({max}) cannot be exceeded. One or more existing "
- "branches must be deleted before a new branch may be created."
+ "The configured maximum number of non-archived branches ({max}) cannot be exceeded. One or "
+ "more existing branches must be deleted before a new branch may be created."
).format(max=max_branches)
)
@@ -201,6 +201,8 @@ def get_unsynced_changes(self):
"""
Return a queryset of all ObjectChange records created in main since the Branch was last synced or created.
"""
+ if self.status not in BranchStatusChoices.WORKING:
+ return ObjectChange.objects.none()
return ObjectChange.objects.using(DEFAULT_DB_ALIAS).exclude(
application__branch=self
).filter(
@@ -212,7 +214,7 @@ def get_unmerged_changes(self):
"""
Return a queryset of all unmerged ObjectChange records within the Branch schema.
"""
- if self.status == BranchStatusChoices.MERGED:
+ if self.status not in BranchStatusChoices.WORKING:
return ObjectChange.objects.none()
return ObjectChange.objects.using(self.connection_name)
@@ -220,7 +222,7 @@ def get_merged_changes(self):
"""
Return a queryset of all merged ObjectChange records for the Branch.
"""
- if self.status != BranchStatusChoices.MERGED:
+ if self.status not in (BranchStatusChoices.MERGED, BranchStatusChoices.ARCHIVED):
return ObjectChange.objects.none()
return ObjectChange.objects.using(DEFAULT_DB_ALIAS).filter(
application__branch=self
@@ -251,6 +253,9 @@ def sync(self, user, commit=True):
if not self.ready:
raise Exception(f"Branch {self} is not ready to sync")
+ # Emit pre-sync signal
+ pre_sync.send(sender=self.__class__, branch=self, user=user)
+
# Retrieve unsynced changes before we update the Branch's status
if changes := self.get_unsynced_changes().order_by('time'):
logger.info(f"Found {len(changes)} changes to sync")
@@ -288,8 +293,8 @@ def sync(self, user, commit=True):
logger.debug(f"Recording branch event: {BranchEventTypeChoices.SYNCED}")
BranchEvent.objects.create(branch=self, user=user, type=BranchEventTypeChoices.SYNCED)
- # Emit branch_synced signal
- branch_synced.send(sender=self.__class__, branch=self, user=user)
+ # Emit post-sync signal
+ post_sync.send(sender=self.__class__, branch=self, user=user)
logger.info('Syncing completed')
@@ -306,6 +311,9 @@ def merge(self, user, commit=True):
if not self.ready:
raise Exception(f"Branch {self} is not ready to merge")
+ # Emit pre-merge signal
+ pre_merge.send(sender=self.__class__, branch=self, user=user)
+
# Retrieve staged changes before we update the Branch's status
if changes := self.get_unmerged_changes().order_by('time'):
logger.info(f"Found {len(changes)} changes to merge")
@@ -354,8 +362,8 @@ def merge(self, user, commit=True):
logger.debug(f"Recording branch event: {BranchEventTypeChoices.MERGED}")
BranchEvent.objects.create(branch=self, user=user, type=BranchEventTypeChoices.MERGED)
- # Emit branch_merged signal
- branch_merged.send(sender=self.__class__, branch=self, user=user)
+ # Emit post-merge signal
+ post_merge.send(sender=self.__class__, branch=self, user=user)
logger.info('Merging completed')
@@ -375,6 +383,9 @@ def revert(self, user, commit=True):
if not self.merged:
raise Exception(f"Only merged branches can be reverted.")
+ # Emit pre-revert signal
+ pre_revert.send(sender=self.__class__, branch=self, user=user)
+
# Retrieve applied changes before we update the Branch's status
if changes := self.get_changes().order_by('-time'):
logger.info(f"Found {len(changes)} changes to revert")
@@ -423,8 +434,8 @@ def revert(self, user, commit=True):
logger.debug(f"Recording branch event: {BranchEventTypeChoices.REVERTED}")
BranchEvent.objects.create(branch=self, user=user, type=BranchEventTypeChoices.REVERTED)
- # Emit branch_reverted signal
- branch_reverted.send(sender=self.__class__, branch=self, user=user)
+ # Emit post-revert signal
+ post_revert.send(sender=self.__class__, branch=self, user=user)
logger.info('Reversion completed')
@@ -440,6 +451,9 @@ def provision(self, user):
logger = logging.getLogger('netbox_branching.branch.provision')
logger.info(f'Provisioning branch {self} ({self.schema_name})')
+ # Emit pre-provision signal
+ pre_provision.send(sender=self.__class__, branch=self, user=user)
+
# Update Branch status
Branch.objects.filter(pk=self.pk).update(status=BranchStatusChoices.PROVISIONING)
@@ -516,8 +530,8 @@ def provision(self, user):
raise e
- # Emit branch_provisioned signal
- branch_provisioned.send(sender=self.__class__, branch=self, user=user)
+ # Emit post-provision signal
+ post_provision.send(sender=self.__class__, branch=self, user=user)
logger.info('Provisioning completed')
@@ -545,6 +559,9 @@ def deprovision(self):
logger = logging.getLogger('netbox_branching.branch.provision')
logger.info(f'Deprovisioning branch {self} ({self.schema_name})')
+ # Emit pre-deprovision signal
+ pre_deprovision.send(sender=self.__class__, branch=self)
+
with connection.cursor() as cursor:
# Delete the schema and all its tables
logger.debug(f'Deleting schema {self.schema_name}')
@@ -552,8 +569,8 @@ def deprovision(self):
f"DROP SCHEMA IF EXISTS {self.schema_name} CASCADE"
)
- # Emit branch_deprovisioned signal
- branch_deprovisioned.send(sender=self.__class__, branch=self)
+ # Emit post-deprovision signal
+ post_deprovision.send(sender=self.__class__, branch=self)
logger.info('Deprovisioning completed')
diff --git a/netbox_branching/signal_receivers.py b/netbox_branching/signal_receivers.py
index f3d79c6..026e6a6 100644
--- a/netbox_branching/signal_receivers.py
+++ b/netbox_branching/signal_receivers.py
@@ -124,11 +124,11 @@ def handle_branch_event(event_type, branch, user=None, **kwargs):
)
-branch_provisioned.connect(partial(handle_branch_event, event_type=BRANCH_PROVISIONED))
-branch_synced.connect(partial(handle_branch_event, event_type=BRANCH_SYNCED))
-branch_merged.connect(partial(handle_branch_event, event_type=BRANCH_MERGED))
-branch_reverted.connect(partial(handle_branch_event, event_type=BRANCH_REVERTED))
-branch_deprovisioned.connect(partial(handle_branch_event, event_type=BRANCH_DEPROVISIONED))
+post_provision.connect(partial(handle_branch_event, event_type=BRANCH_PROVISIONED))
+post_deprovision.connect(partial(handle_branch_event, event_type=BRANCH_DEPROVISIONED))
+post_sync.connect(partial(handle_branch_event, event_type=BRANCH_SYNCED))
+post_merge.connect(partial(handle_branch_event, event_type=BRANCH_MERGED))
+post_revert.connect(partial(handle_branch_event, event_type=BRANCH_REVERTED))
@receiver(pre_delete, sender=Branch)
diff --git a/netbox_branching/signals.py b/netbox_branching/signals.py
index cbc99d6..063f6c4 100644
--- a/netbox_branching/signals.py
+++ b/netbox_branching/signals.py
@@ -1,26 +1,28 @@
from django.dispatch import Signal
-from .events import *
-
__all__ = (
- 'branch_deprovisioned',
- 'branch_merged',
- 'branch_provisioned',
- 'branch_reverted',
- 'branch_synced',
+ 'post_deprovision',
+ 'post_merge',
+ 'post_provision',
+ 'post_revert',
+ 'post_sync',
+ 'pre_deprovision',
+ 'pre_merge',
+ 'pre_provision',
+ 'pre_revert',
+ 'pre_sync',
)
+# Pre-event signals
+pre_provision = Signal()
+pre_deprovision = Signal()
+pre_sync = Signal()
+pre_merge = Signal()
+pre_revert = Signal()
-branch_provisioned = Signal()
-branch_deprovisioned = Signal()
-branch_synced = Signal()
-branch_merged = Signal()
-branch_reverted = Signal()
-
-branch_signals = {
- branch_provisioned: BRANCH_PROVISIONED,
- branch_deprovisioned: BRANCH_DEPROVISIONED,
- branch_synced: BRANCH_SYNCED,
- branch_merged: BRANCH_MERGED,
- branch_reverted: BRANCH_REVERTED,
-}
+# Post-event signals
+post_provision = Signal()
+post_deprovision = Signal()
+post_sync = Signal()
+post_merge = Signal()
+post_revert = Signal()
diff --git a/netbox_branching/tables/tables.py b/netbox_branching/tables/tables.py
index efdecd0..f1df9e6 100644
--- a/netbox_branching/tables/tables.py
+++ b/netbox_branching/tables/tables.py
@@ -83,9 +83,9 @@ def render_is_active(self, value):
class ChangeDiffTable(NetBoxTable):
- name = tables.Column(
- verbose_name=_('Name'),
- linkify=True
+ # TODO: Revert to the default "id" column when a detail view for ChangeDiff is implemented
+ id = tables.Column(
+ verbose_name=_('ID')
)
branch = tables.Column(
verbose_name=_('Branch'),
@@ -122,8 +122,8 @@ class ChangeDiffTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ChangeDiff
fields = (
- 'branch', 'object_type', 'object', 'action', 'conflicts', 'original_diff', 'modified_diff', 'current_diff',
- 'last_updated', 'actions',
+ 'id', 'branch', 'object_type', 'object', 'action', 'conflicts', 'original_diff', 'modified_diff',
+ 'current_diff', 'last_updated', 'actions',
)
default_columns = ('branch', 'object', 'action', 'conflicts', 'original_diff', 'modified_diff', 'current_diff')
diff --git a/netbox_branching/templates/netbox_branching/branch.html b/netbox_branching/templates/netbox_branching/branch.html
index 75edc00..75eff0c 100644
--- a/netbox_branching/templates/netbox_branching/branch.html
+++ b/netbox_branching/templates/netbox_branching/branch.html
@@ -4,6 +4,7 @@
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
+{% load branch_buttons %}
{% block extra_controls %}
{% if not object.is_active %}
@@ -22,44 +23,12 @@
{% endif %}
{% if object.ready %}
- {% if perms.netbox_branching.sync_branch %}
-
- {% trans "Sync" %}
-
- {% else %}
-
- {% endif %}
- {% if perms.netbox_branching.merge_branch %}
-
- {% trans "Merge" %}
-
- {% else %}
-
- {% endif %}
+ {% branch_sync_button object %}
+ {% branch_merge_button object %}
{% endif %}
{% if object.merged %}
- {% if perms.netbox_branching.revert_branch %}
-
- {% trans "Revert" %}
-
- {% else %}
-
- {% endif %}
- {% if perms.netbox_branching.archive_branch %}
-
- {% trans "Archive" %}
-
- {% else %}
-
- {% endif %}
+ {% branch_revert_button object %}
+ {% branch_archive_button object %}
{% endif %}
{% endblock %}
diff --git a/netbox_branching/templates/netbox_branching/buttons/branch_archive.html b/netbox_branching/templates/netbox_branching/buttons/branch_archive.html
new file mode 100644
index 0000000..1ecbf34
--- /dev/null
+++ b/netbox_branching/templates/netbox_branching/buttons/branch_archive.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{% if perms.netbox_branching.archive_branch %}
+
+ {% trans "Archive" %}
+
+{% else %}
+
+{% endif %}
diff --git a/netbox_branching/templates/netbox_branching/buttons/branch_merge.html b/netbox_branching/templates/netbox_branching/buttons/branch_merge.html
new file mode 100644
index 0000000..efbb5b0
--- /dev/null
+++ b/netbox_branching/templates/netbox_branching/buttons/branch_merge.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{% if perms.netbox_branching.merge_branch %}
+
+ {% trans "Merge" %}
+
+{% else %}
+
+{% endif %}
diff --git a/netbox_branching/templates/netbox_branching/buttons/branch_revert.html b/netbox_branching/templates/netbox_branching/buttons/branch_revert.html
new file mode 100644
index 0000000..f004631
--- /dev/null
+++ b/netbox_branching/templates/netbox_branching/buttons/branch_revert.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{% if perms.netbox_branching.revert_branch %}
+
+ {% trans "Revert" %}
+
+{% else %}
+
+{% endif %}
diff --git a/netbox_branching/templates/netbox_branching/buttons/branch_sync.html b/netbox_branching/templates/netbox_branching/buttons/branch_sync.html
new file mode 100644
index 0000000..fcf21bb
--- /dev/null
+++ b/netbox_branching/templates/netbox_branching/buttons/branch_sync.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{% if perms.netbox_branching.sync_branch %}
+
+ {% trans "Sync" %}
+
+{% else %}
+
+{% endif %}
diff --git a/netbox_branching/templatetags/__init__.py b/netbox_branching/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_branching/templatetags/branch_buttons.py b/netbox_branching/templatetags/branch_buttons.py
new file mode 100644
index 0000000..2fa62f2
--- /dev/null
+++ b/netbox_branching/templatetags/branch_buttons.py
@@ -0,0 +1,42 @@
+from django import template
+
+__all__ = (
+ 'branch_sync_button',
+ 'branch_merge_button',
+ 'branch_revert_button',
+ 'branch_archive_button',
+)
+
+register = template.Library()
+
+
+@register.inclusion_tag('netbox_branching/buttons/branch_sync.html', takes_context=True)
+def branch_sync_button(context, branch):
+ return {
+ 'branch': branch,
+ 'perms': context.get('perms'),
+ }
+
+
+@register.inclusion_tag('netbox_branching/buttons/branch_merge.html', takes_context=True)
+def branch_merge_button(context, branch):
+ return {
+ 'branch': branch,
+ 'perms': context.get('perms'),
+ }
+
+
+@register.inclusion_tag('netbox_branching/buttons/branch_revert.html', takes_context=True)
+def branch_revert_button(context, branch):
+ return {
+ 'branch': branch,
+ 'perms': context.get('perms'),
+ }
+
+
+@register.inclusion_tag('netbox_branching/buttons/branch_archive.html', takes_context=True)
+def branch_archive_button(context, branch):
+ return {
+ 'branch': branch,
+ 'perms': context.get('perms'),
+ }
diff --git a/netbox_branching/tests/test_api.py b/netbox_branching/tests/test_api.py
index 3e63781..341dacd 100644
--- a/netbox_branching/tests/test_api.py
+++ b/netbox_branching/tests/test_api.py
@@ -9,6 +9,7 @@
from dcim.models import Site
from users.models import Token
+from netbox_branching.constants import COOKIE_NAME
from netbox_branching.models import Branch
@@ -58,21 +59,43 @@ def test_without_branch(self):
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], 'Site 1')
- def test_with_branch(self):
+ def test_with_branch_header(self):
+ url = reverse('dcim-api:site-list')
branch = Branch.objects.first()
self.assertIsNotNone(branch, "Branch was not created")
+
+ # Regular API query
+ response = self.client.get(url, **self.header)
+ results = self.get_results(response)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['name'], 'Site 1')
+
+ # Branch-aware API query
header = {
**self.header,
f'HTTP_X_NETBOX_BRANCH': branch.schema_id,
}
+ response = self.client.get(url, **header)
+ results = self.get_results(response)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['name'], 'Site 2')
- # Sanity checks
- self.assertEqual(Site.objects.count(), 1)
- self.assertEqual(Site.objects.using(branch.connection_name).count(), 1)
-
+ def test_with_branch_cookie(self):
url = reverse('dcim-api:site-list')
- response = self.client.get(url, **header)
+ branch = Branch.objects.first()
+ self.assertIsNotNone(branch, "Branch was not created")
+
+ # Regular API query
+ response = self.client.get(url, **self.header)
results = self.get_results(response)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['name'], 'Site 1')
+ # Branch-aware API query
+ self.client.cookies.load({
+ COOKIE_NAME: branch.schema_id,
+ })
+ response = self.client.get(url, **self.header)
+ results = self.get_results(response)
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], 'Site 2')
diff --git a/netbox_branching/tests/test_branches.py b/netbox_branching/tests/test_branches.py
index 6cbd01d..3bb263b 100644
--- a/netbox_branching/tests/test_branches.py
+++ b/netbox_branching/tests/test_branches.py
@@ -112,10 +112,16 @@ def test_max_branches(self):
Verify that the max_branches config parameter is enforced.
"""
Branch.objects.bulk_create((
- Branch(name='Branch 1'),
- Branch(name='Branch 2'),
+ Branch(name='Branch 1', status=BranchStatusChoices.ARCHIVED),
+ Branch(name='Branch 2', status=BranchStatusChoices.READY),
))
+ # Creating a second non-archived Branch should succeed
branch = Branch(name='Branch 3')
+ branch.full_clean()
+ branch.save(provision=False)
+
+ # Creating a third non-archived Branch should fail
+ branch = Branch(name='Branch 4')
with self.assertRaises(ValidationError):
branch.full_clean()
diff --git a/netbox_branching/utilities.py b/netbox_branching/utilities.py
index 74431b2..ea38cea 100644
--- a/netbox_branching/utilities.py
+++ b/netbox_branching/utilities.py
@@ -4,7 +4,9 @@
from dataclasses import dataclass
from django.db.models import ForeignKey, ManyToManyField
+from django.urls import reverse
+from .constants import REPLICATE_TABLES
from .contextvars import active_branch
__all__ = (
@@ -15,6 +17,7 @@
'deactivate_branch',
'get_branchable_object_types',
'get_tables_to_replicate',
+ 'is_api_request',
'record_applied_change',
'update_object',
)
@@ -79,9 +82,9 @@ def get_branchable_object_types():
def get_tables_to_replicate():
"""
- Returned an ordered list of database tables to replicate when provisioning a new schema.
+ Return an ordered list of database tables to replicate when provisioning a new schema.
"""
- tables = set()
+ tables = set(REPLICATE_TABLES)
branch_aware_models = [
ot.model_class() for ot in get_branchable_object_types()
@@ -166,3 +169,10 @@ def record_applied_change(instance, branch, **kwargs):
from .models import AppliedChange
AppliedChange.objects.update_or_create(change=instance, defaults={'branch': branch})
+
+
+def is_api_request(request):
+ """
+ Returns True if the given request is a REST or GraphQL API request.
+ """
+ return request.path_info.startswith(reverse('api-root')) or request.path_info.startswith(reverse('graphql'))
diff --git a/pyproject.toml b/pyproject.toml
index f20d422..f1d8b91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "netboxlabs-netbox-branching"
-version = "0.5.0"
+version = "0.5.1"
description = "A git-like branching implementation for NetBox"
readme = "README.md"
requires-python = ">=3.10"