Skip to content

Commit 1659ed2

Browse files
committed
add the contract tab
1 parent 0c0cf65 commit 1659ed2

6 files changed

Lines changed: 106 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
### Version 2.4.5
99
* [292](https://github.com/mlebreuil/netbox-contract/issues/292) bug fix. Contract detail show internal party value instead of label.
1010

11+
* [294](https://github.com/mlebreuil/netbox-contract/issues/294) Add `contract_assignments_display` plugin setting (`tab`, `inline`, `both`) for contract assignment UI. The tab view allows customization of the contracts list table columns.
1112

1213
### Version 2.4.4
1314
* [288](https://github.com/mlebreuil/netbox-contract/issues/288) Add the possibility to assign contract any object type. By default the following objects types: 'circuits.circuit', 'circuits.virtualcircuit', 'dcim.site', 'dcim.device', 'dcim.rack', 'virtualization.virtualmachine', 'virtualization.cluster', 'ipam.ipaddress', 'ipam.prefix'. This list can be overriden within the PLUGINS_CONFIG configuration parameter. Check the README file or [documentation](https://mlebreuil.github.io/netbox-contract/) for more information.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,17 @@ PLUGINS_CONFIG = {
7474
'ipam.ipaddress',
7575
'ipam.prefix',
7676
],
77+
'contract_assignments_display': 'both', # options: 'tab', 'inline', 'both'
7778
}
7879
}
7980

8081
```
8182

8283
* top_level_menu : If "Contracts" appears under the "Plugins" menu item or on its own
84+
* contract_assignments_display:
85+
* `'tab'` - only the Contracts view tab is visible on the related object page.
86+
* `'inline'` - only the inline contract assignments table is shown in object detail; the Contracts tab is hidden.
87+
* `'both'` - both tab and inline table are shown (default behavior).
8388
* default_accounting_dimensions: The accounting dimensions which will appear in the field' background when empty. Note that accounting dimensions are now managed as individual objects. The use of this field is deprecated.
8489
* mandatory_contract_fields, mandatory_invoice_fields: Fields which are not required by default and can be set as such. The list of fields is at the bottom of the contract import form.
8590
* hidden_contract_fields, hidden_invoice_fields: List of fields to be hidden. Fields should not be required to be hidden.

netbox_contract/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ContractsConfig(PluginConfig):
2727
'virtualization.virtualmachine',
2828
'virtualization.cluster',
2929
],
30+
'contract_assignments_display': 'both', # options: 'tab', 'inline', 'both'
3031
}
3132

3233

netbox_contract/template_content.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.conf import settings
12
from django.contrib.contenttypes.models import ContentType
23
from netbox.plugins import PluginTemplateExtension
34

@@ -10,22 +11,34 @@ class ObjectContractAssignments(PluginTemplateExtension):
1011
models = ASSIGNEMENT_TYPES
1112

1213
def full_width_page(self):
13-
object = self.context['object']
14-
object_type = ContentType.objects.get_for_model(object)
15-
16-
contract_assignments = ContractAssignment.objects.filter(
17-
content_type__pk=object_type.id, object_id=object.id
18-
)
19-
assignments_table = tables.ContractAssignmentObjectTable(contract_assignments)
20-
assignments_table.configure(self.context['request'])
21-
22-
return self.render(
23-
'contract_assignments_bottom.html',
24-
extra_context={
25-
'assignments_table': assignments_table,
26-
},
14+
display = settings.PLUGINS_CONFIG.get('netbox_contract', {}).get(
15+
'contract_assignments_display', 'both'
2716
)
2817

18+
# 'tab' means only the contract tab is shown; inline table stays hidden.
19+
if display == 'tab':
20+
return ''
21+
22+
# 'inline' or 'both' renders inline table; the tab is still built via ObjectChildrenView.
23+
if display in ('inline', 'both'):
24+
object = self.context['object']
25+
object_type = ContentType.objects.get_for_model(object)
26+
27+
contract_assignments = ContractAssignment.objects.filter(
28+
content_type__pk=object_type.id, object_id=object.id
29+
)
30+
assignments_table = tables.ContractAssignmentObjectTable(contract_assignments)
31+
assignments_table.configure(self.context['request'])
32+
33+
return self.render(
34+
'contract_assignments_bottom.html',
35+
extra_context={
36+
'assignments_table': assignments_table,
37+
},
38+
)
39+
40+
return ''
41+
2942

3043
template_extensions = [
3144
ObjectContractAssignments,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% extends 'generic/object_children.html' %}
2+
{% load buttons %}
3+
4+
{% block table_controls %}
5+
{# Only the page header action button is needed (avoid duplicate table button) #}
6+
{{ block.super }}
7+
{% endblock table_controls %}

netbox_contract/views.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from datetime import date, timedelta
22

33
from dateutil.relativedelta import relativedelta
4+
from django.apps import apps
45
from django.conf import settings
56
from django.contrib.contenttypes.models import ContentType
67
from django.core.exceptions import ObjectDoesNotExist
78
from django.db.models import Case, F, When
89
from django.db.models.functions import Round
910
from django.shortcuts import get_object_or_404, render
11+
from django.utils.translation import gettext_lazy as _
12+
from netbox.object_actions import *
1013
from netbox.views import generic
1114
from netbox.views.generic.utils import get_prerequisite_model
1215
from utilities.forms import restrict_form_fields
1316
from utilities.querydict import normalize_querydict
14-
from utilities.views import register_model_view
17+
from utilities.views import ViewTab, get_action_url, register_model_view
1518

1619
from . import filtersets, forms, tables
20+
from .constants import ASSIGNEMENT_TYPES
1721
from .models import (
1822
AccountingDimension,
1923
Contract,
@@ -182,6 +186,66 @@ class ContractAssignmentBulkDeleteView(generic.BulkDeleteView):
182186
filterset = filtersets.ContractAssignmentFilterSet
183187
table = tables.ContractAssignmentListTable
184188

189+
190+
class AddContractAssignment(AddObject):
191+
label = _('Add contract')
192+
193+
@classmethod
194+
def get_url(cls, obj):
195+
# obj will be the parent object in the custom template (ObjectChildren hook)
196+
if hasattr(obj, 'pk') and hasattr(obj, '_meta') and obj.pk:
197+
parent = obj
198+
parent_ct = ContentType.objects.get_for_model(parent)
199+
base_url = get_action_url(ContractAssignment, action='add')
200+
return (
201+
f"{base_url}?content_type={parent_ct.pk}"
202+
f"&object_id={parent.pk}&return_url={parent.get_absolute_url()}"
203+
)
204+
205+
# fallback for a class value (if called as model class)
206+
return get_action_url(ContractAssignment, action='add')
207+
208+
209+
class BaseObjectContractAssignmentView(generic.ObjectChildrenView):
210+
child_model = ContractAssignment
211+
table = tables.ContractAssignmentObjectTable
212+
filterset = filtersets.ContractAssignmentFilterSet
213+
template_name = 'netbox_contract/object_contracts.html'
214+
actions = (AddContractAssignment, BulkEdit, BulkDelete)
215+
tab = ViewTab(
216+
label=_('Contracts'),
217+
visible=lambda obj: plugin_settings.get('contract_assignments_display', 'both') != 'inline',
218+
badge=lambda obj: ContractAssignment.objects.filter(
219+
content_type=ContentType.objects.get_for_model(obj), object_id=obj.id
220+
).count(),
221+
permission='contracts.view_contractassignment',
222+
weight=550,
223+
hide_if_empty=True,
224+
)
225+
226+
def get_children(self, request, parent):
227+
object_type = ContentType.objects.get_for_model(parent)
228+
contract_assignments = ContractAssignment.objects.filter(
229+
content_type__pk=object_type.id, object_id=parent.id
230+
)
231+
return contract_assignments
232+
233+
234+
# Dynamically register the view for all supported models
235+
for model_string in ASSIGNEMENT_TYPES:
236+
app_label, model_name = model_string.split('.')
237+
model = apps.get_model(app_label, model_name)
238+
239+
class_name = f"{model_name.title()}ContractAssignmentView"
240+
attrs = {
241+
'queryset': model.objects.all(),
242+
'viewname': f'netbox_contract:{model_name}_contracts',
243+
}
244+
view_class = type(class_name, (BaseObjectContractAssignmentView,), attrs)
245+
246+
register_model_view(model, 'contracts', path='contracts')(view_class)
247+
248+
185249
# Contract views
186250

187251

0 commit comments

Comments
 (0)