Skip to content

Commit b961889

Browse files
authored
Merge pull request #90 from Kani999/netbox_4_4
Migration to NetBox v4.4
2 parents 4ea026c + b3384f7 commit b961889

File tree

10 files changed

+163
-38
lines changed

10 files changed

+163
-38
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ __pycache__
55
dist/
66
build/
77
.vscode/
8+
/contrib/
9+
/pynetbox_test.py

README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The following table shows the compatibility between different NetBox versions an
2525
| >= 4.1.0 | 6.x.x |
2626
| >= 4.2.0 | 7.x.x |
2727
| >= 4.3.0 | 8.x.x |
28+
| >= 4.4.0 | 9.x.x |
2829

2930
## Installation
3031

@@ -57,6 +58,22 @@ Restart NetBox and ensure that `netbox-attachments` is included in your `local_r
5758

5859
For more details, see the [NetBox Documentation](https://docs.netbox.dev/en/stable/plugins/#installing-plugins).
5960

61+
## NetBox 4.4 Compatibility Changes
62+
63+
Starting from version 9.0.0, the plugin has been updated for full NetBox 4.4 compatibility with the following changes:
64+
65+
### Template Extension Updates
66+
- **Models Attribute**: Updated template extensions to use the `models` list attribute instead of the deprecated `model` attribute for NetBox 4.x compatibility.
67+
- **Error Handling**: Improved error handling for template rendering when object types are missing.
68+
- **Template Panel Rendering**: Fixed AttributeError issues in `render_attachment_panel` function for proper template extension compatibility.
69+
70+
### API and URL Improvements
71+
- **Bulk Action URLs**: Added proper URL patterns for bulk edit and bulk delete operations.
72+
- **URL Pattern Reorganization**: Improved URL pattern ordering for better routing logic.
73+
- **Default Return URLs**: Enhanced navigation flow after bulk operations.
74+
75+
These changes ensure the plugin works seamlessly with NetBox 4.4 while maintaining all core attachment functionality and improving the user experience.
76+
6077

6178
## New Validation Checks
6279

@@ -104,17 +121,50 @@ The plugin can be customized using the following configuration options:
104121
- **Description**: Override the display settings for specific models.
105122
- **Tip**: Use the correct `app_label` and `model` names, which can be found in the API at `<your_netbox_url>/api/extras/content-types/`.
106123

107-
> ~~**Warning**: The `additional_tab` option does not work for plugin models.~~
124+
125+
## API Usage
126+
127+
Since the import functionality has been removed, you can use the NetBox API to programmatically manage attachments:
128+
129+
### Creating Attachments via API
130+
131+
```python
132+
import requests
133+
134+
# Example: Upload attachment via API
135+
url = "https://your-netbox-url/api/plugins/netbox-attachments/attachments/"
136+
headers = {
137+
"Authorization": "Token your-api-token",
138+
"Content-Type": "application/json"
139+
}
140+
141+
# For file uploads, use multipart/form-data
142+
files = {
143+
'file': ('filename.pdf', open('path/to/file.pdf', 'rb'))
144+
}
145+
data = {
146+
'object_type': 'dcim.device', # ContentType ID or app_label.model
147+
'object_id': 123,
148+
'name': 'Device Manual',
149+
'description': 'User manual for the device'
150+
}
151+
152+
response = requests.post(url, headers=headers, files=files, data=data)
153+
```
108154

109155
> **Note**: The `additional_tab` feature will work for plugin models if you include the following in your `urls.py`:
110156
>
111157
> ```python
158+
> from netbox.urls import get_model_urls
159+
>
112160
> path(
113161
> "MODEL/<int:pk>/",
114162
> include(get_model_urls("plugin_name", "model_name")),
115163
> ),
116164
> ```
117165
>
166+
> **Note**: `plugin_name` refers to the plugin slug used in URLs (often hyphenated), which may differ from the Python package/module name.
167+
>
118168
> By doing so, the system will automatically include the Changelog, Journal, and other registered tabs (such as Attachments) when `additional_tab` is enabled.
119169
120170
### Configuration Example

netbox_attachments/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class NetBoxAttachmentsConfig(PluginConfig):
2525
"display_setting": {},
2626
}
2727
required_settings = []
28-
min_version = "4.3.0"
29-
max_version = "4.3.99"
28+
min_version = "4.4.0"
29+
max_version = "4.4.99"
3030

3131

3232
config = NetBoxAttachmentsConfig

netbox_attachments/api/serializers.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from core.models.contenttypes import ObjectType
1+
from core.models.object_types import ObjectType
22
from django.core.exceptions import ObjectDoesNotExist
33
from netbox.api.fields import ContentTypeField
44
from netbox.api.serializers import NetBoxModelSerializer
@@ -45,17 +45,36 @@ def validate(self, data):
4545
)
4646
)
4747

48-
# Enforce model validation
49-
super().validate(data)
48+
# Enforce model validation and capture any upstream mutations
49+
validated_data = super().validate(data)
5050

51-
return data
51+
return validated_data
5252

5353
# @swagger_serializer_method(serializer_or_field=serializers.JSONField)
5454
def get_parent(self, obj):
55-
if obj.parent:
56-
serializer = get_serializer_for_model(obj.parent)
57-
return serializer(
58-
obj.parent, nested=True, context={"request": self.context["request"]}
59-
).data
60-
else:
55+
# Check for required fields first to avoid unnecessary DB lookups
56+
if not (
57+
hasattr(obj, "object_type_id")
58+
and obj.object_type_id
59+
and hasattr(obj, "object_id")
60+
and obj.object_id
61+
):
6162
return None
63+
64+
# Only wrap the parent dereference in try/except to let other errors bubble
65+
try:
66+
parent = obj.parent
67+
except ObjectDoesNotExist:
68+
# Handle case where parent object doesn't exist
69+
return None
70+
71+
if parent is None:
72+
return None
73+
74+
# Get serializer for the parent's class, not the instance
75+
serializer = get_serializer_for_model(parent.__class__)
76+
return serializer(
77+
parent,
78+
nested=True,
79+
context=self.context,
80+
).data

netbox_attachments/forms.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
from core.models.contenttypes import ObjectType
1+
from core.models.object_types import ObjectType
22
from django import forms
33
from django.utils.translation import gettext as _
4-
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
4+
from netbox.forms import (
5+
NetBoxModelBulkEditForm,
6+
NetBoxModelFilterSetForm,
7+
NetBoxModelForm,
8+
)
59
from utilities.forms.fields import (
610
CommentField,
711
DynamicModelMultipleChoiceField,
@@ -58,3 +62,17 @@ class NetBoxAttachmentFilterForm(NetBoxModelFilterSetForm):
5862
),
5963
)
6064
tag = TagFilterField(model)
65+
66+
67+
class NetBoxAttachmentBulkEditForm(NetBoxModelBulkEditForm):
68+
name = forms.CharField(
69+
max_length=NetBoxAttachment._meta.get_field("name").max_length, required=False
70+
)
71+
description = forms.CharField(
72+
widget=forms.Textarea,
73+
max_length=NetBoxAttachment._meta.get_field("description").max_length,
74+
required=False,
75+
)
76+
77+
model = NetBoxAttachment
78+
nullable_fields = ("name", "description")

netbox_attachments/models.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numbers
22
import os
33

4-
from core.models.contenttypes import ObjectType
4+
from core.models.object_types import ObjectType
55
from django.core.exceptions import ObjectDoesNotExist, ValidationError
66
from django.db import models
77
from django.db.models.signals import pre_delete
@@ -57,14 +57,18 @@ def filename(self):
5757

5858
@property
5959
def parent(self):
60+
# Guard using object_type_id and object_id
61+
if not (self.object_type_id and self.object_id):
62+
return None
63+
6064
if self.object_type.model_class() is None:
61-
# Model for the content type does not exists
62-
# Model was probably deleted or uninstalled -> parent object cannot be found
65+
# Model was probably deleted or uninstalled — parent object cannot be found
6366
return None
67+
6468
try:
6569
return self.object_type.get_object_for_this_type(id=self.object_id)
6670
except ObjectDoesNotExist:
67-
# Object for the content type Model does not exists
71+
# Handle case where parent object doesn't exist
6872
return None
6973

7074
def get_absolute_url(self):

netbox_attachments/template_content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from typing import List, Type
33

4-
from core.models.contenttypes import ObjectType
4+
from core.models.object_types import ObjectType
55
from django.apps import apps
66
from django.conf import settings
77
from django.db.models import Model

netbox_attachments/urls.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
from django.urls import path
2-
from netbox.views.generic import ObjectChangeLogView
1+
from django.urls import include, path
2+
from utilities.urls import get_model_urls
33

4-
from netbox_attachments import models, views
4+
from netbox_attachments import views
55

66
urlpatterns = (
7-
# Files
7+
path(
8+
"netbox-attachments/",
9+
views.NetBoxAttachmentListView.as_view(),
10+
name="netboxattachment_list",
11+
),
812
path(
913
"netbox-attachments/add/",
1014
views.NetBoxAttachmentEditView.as_view(),
1115
name="netboxattachment_add",
1216
),
17+
path(
18+
"netbox-attachments/edit/",
19+
views.NetBoxAttachmentBulkEditView.as_view(),
20+
name="netboxattachment_bulk_edit",
21+
),
22+
path(
23+
"netbox-attachments/delete/",
24+
views.NetBoxAttachmentBulkDeleteView.as_view(),
25+
name="netboxattachment_bulk_delete",
26+
),
27+
path(
28+
"netbox-attachments/<int:pk>/",
29+
views.NetBoxAttachmentView.as_view(),
30+
name="netboxattachment",
31+
),
1332
path(
1433
"netbox-attachments/<int:pk>/edit/",
1534
views.NetBoxAttachmentEditView.as_view(),
@@ -22,18 +41,10 @@
2241
),
2342
path(
2443
"netbox-attachments/",
25-
views.NetBoxAttachmentListView.as_view(),
26-
name="netboxattachment_list",
44+
include(get_model_urls("netbox_attachments", "netboxattachment", detail=False)),
2745
),
2846
path(
2947
"netbox-attachments/<int:pk>/",
30-
views.NetBoxAttachmentView.as_view(),
31-
name="netboxattachment",
32-
),
33-
path(
34-
"netbox-attachments/<int:pk>/changelog/",
35-
ObjectChangeLogView.as_view(),
36-
name="netboxattachment_changelog",
37-
kwargs={"model": models.NetBoxAttachment},
48+
include(get_model_urls("netbox_attachments", "netboxattachment")),
3849
),
3950
)

netbox_attachments/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "8.0.4"
1+
__version__ = "9.0.0"

netbox_attachments/views.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
from core.models.contenttypes import ObjectType
1+
from core.models.object_types import ObjectType
22
from django.shortcuts import get_object_or_404
33
from netbox.views import generic
4+
from utilities.views import register_model_view
45

56
from netbox_attachments import filtersets, forms, models, tables
67

78

9+
@register_model_view(models.NetBoxAttachment, name="", detail=True)
810
class NetBoxAttachmentView(generic.ObjectView):
9-
controls = []
1011
queryset = models.NetBoxAttachment.objects.all()
1112

1213

14+
@register_model_view(models.NetBoxAttachment, name="list", path="", detail=False)
1315
class NetBoxAttachmentListView(generic.ObjectListView):
1416
actions = {
15-
"import": {"add"},
1617
"export": set(),
1718
"bulk_edit": {"change"},
1819
"bulk_delete": {"delete"},
@@ -23,6 +24,8 @@ class NetBoxAttachmentListView(generic.ObjectListView):
2324
filterset_form = forms.NetBoxAttachmentFilterForm
2425

2526

27+
@register_model_view(models.NetBoxAttachment, name="add", detail=False)
28+
@register_model_view(models.NetBoxAttachment, name="edit", detail=True)
2629
class NetBoxAttachmentEditView(generic.ObjectEditView):
2730
queryset = models.NetBoxAttachment.objects.all()
2831
form = forms.NetBoxAttachmentForm
@@ -46,6 +49,24 @@ def get_extra_addanother_params(self, request):
4649
}
4750

4851

52+
@register_model_view(models.NetBoxAttachment, name="delete", detail=True)
4953
class NetBoxAttachmentDeleteView(generic.ObjectDeleteView):
5054
queryset = models.NetBoxAttachment.objects.all()
55+
56+
57+
@register_model_view(models.NetBoxAttachment, "bulk_edit", path="edit", detail=False)
58+
class NetBoxAttachmentBulkEditView(generic.BulkEditView):
59+
queryset = models.NetBoxAttachment.objects.all()
60+
filterset = filtersets.NetBoxAttachmentFilterSet
61+
table = tables.NetBoxAttachmentTable
62+
form = forms.NetBoxAttachmentBulkEditForm
63+
64+
65+
@register_model_view(
66+
models.NetBoxAttachment, "bulk_delete", path="delete", detail=False
67+
)
68+
class NetBoxAttachmentBulkDeleteView(generic.BulkDeleteView):
69+
queryset = models.NetBoxAttachment.objects.all()
70+
filterset = filtersets.NetBoxAttachmentFilterSet
71+
table = tables.NetBoxAttachmentTable
5172
default_return_url = "plugins:netbox_attachments:netboxattachment_list"

0 commit comments

Comments
 (0)