diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 594f23f9a..16182af64 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.5 + placeholder: v3.1.6 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index b1193ae02..0be999b16 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.5 + placeholder: v3.1.6 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index cbc893aa9..aaa9c7f44 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -98,10 +98,6 @@ psycopg2-binary # https://github.com/yaml/pyyaml PyYAML -# In-memory key/value store used for caching and queuing -# https://github.com/andymccurdy/redis-py -redis - # Social authentication framework # https://github.com/python-social-auth/social-core social-auth-core[all] diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index acf13b82f..742e93804 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -114,6 +114,12 @@ This ensures that your development environment is now complete and operational. !!! info "IDE Integration" Some IDEs, such as PyCharm, will integrate with Django's development server and allow you to run it directly within the IDE. This is strongly encouraged as it makes for a much more convenient development environment. +## Populating Demo Data + +Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at .) + +The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data. + ## Running Tests Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command. diff --git a/docs/index.md b/docs/index.md index 7abbd9310..943f1d7ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and | Application | Django/Python | | Database | PostgreSQL 10+ | | Task queuing | Redis/django-rq | -| Live device access | NAPALM | +| Live device access | NAPALM (optional) | ## Supported Python Versions @@ -58,4 +58,6 @@ NetBox supports Python 3.7, 3.8, and 3.9 environments currently. (Support for Py ## Getting Started -See the [installation guide](installation/index.md) for help getting NetBox up and running quickly. +Minor NetBox releases (e.g. v3.1) are published three times a year; in April, August, and December. These typically introduce major new features and may contain breaking API changes. Patch releases are published roughly every one to two weeks to resolve bugs and fulfill minor feature requests. These are backward-compatible with previous releases unless otherwise noted. The NetBox maintainers strongly recommend running the latest stable release whenever possible. + +Please see the [official installation guide](installation/index.md) for detailed instructions on obtaining and installing NetBox. diff --git a/docs/plugins/development.md b/docs/plugins/development.md index cde659a45..9d1ad1444 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -1,5 +1,8 @@ # Plugin Development +!!! info "Help Improve the NetBox Plugins Framework!" + We're looking for volunteers to help improve NetBox's plugins framework. If you have experience developing plugins, we'd love to hear from you! You can find more information about this initiative [here](https://github.com/netbox-community/netbox/discussions/8338). + This documentation covers the development of custom plugins for NetBox. Plugins are essentially self-contained [Django apps](https://docs.djangoproject.com/en/stable/) which integrate with NetBox to provide custom functionality. Since the development of Django apps is already very well-documented, we'll only be covering the aspects that are specific to NetBox. Plugins can do a lot, including: diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 7caa1e3ab..13c129398 100644 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -1,6 +1,14 @@ # Release Notes -Listed below are the major features introduced in each NetBox release. For more detail on a specific release train, see its individual release notes page. +NetBox releases are numbered as major, minor, and patch releases. For example, version 3.1.0 is a minor release, and v3.1.5 is a patch release. Briefly, these can be described as follows: + +* **Major** - Introduces or removes an entire API or other core functionality +* **Minor** - Implements major new features but may include breaking changes for API consumers or other integrations +* **Patch** - A maintenance release which fixes bugs and may introduce backward-compatible enhancements + +Minor releases are published in April, August, and December of each calendar year. Patch releases are published as needed to address bugs and fulfill minor feature requests, typically around every one to two weeks. + +This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release. #### [Version 3.1](./version-3.1.md) (December 2021) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index eb2a8c9dd..88a6da77f 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,5 +1,33 @@ # NetBox v3.1 +## v3.1.6 (2022-01-17) + +### Enhancements + +* [#8246](https://github.com/netbox-community/netbox/issues/8246) - Show human-friendly values for commit rates in circuits table +* [#8262](https://github.com/netbox-community/netbox/issues/8262) - Add cable count to tenant stats +* [#8265](https://github.com/netbox-community/netbox/issues/8265) - Add Stackwise-n interface types +* [#8293](https://github.com/netbox-community/netbox/issues/8293) - Show 4-byte ASNs in ASDOT notation +* [#8302](https://github.com/netbox-community/netbox/issues/8302) - Linkify role column in device & VM tables +* [#8337](https://github.com/netbox-community/netbox/issues/8337) - Enable sorting object tables by created & updated times + +### Bug Fixes + +* [#8279](https://github.com/netbox-community/netbox/issues/8279) - Fix display of virtual chassis members in rack elevations +* [#8285](https://github.com/netbox-community/netbox/issues/8285) - Fix `cluster_count` under tenant REST API serializer +* [#8287](https://github.com/netbox-community/netbox/issues/8287) - Correct label in export template form +* [#8301](https://github.com/netbox-community/netbox/issues/8301) - Fix delete button for various object children views +* [#8305](https://github.com/netbox-community/netbox/issues/8305) - Fix assignment of custom field data to FHRP groups via UI +* [#8306](https://github.com/netbox-community/netbox/issues/8306) - Redirect user to previous page after login +* [#8314](https://github.com/netbox-community/netbox/issues/8314) - Prevent custom fields with default values from appearing as applied filters erroneously +* [#8317](https://github.com/netbox-community/netbox/issues/8317) - Fix CSV import of multi-select custom field values +* [#8319](https://github.com/netbox-community/netbox/issues/8319) - Custom URL fields should honor `ALLOWED_URL_SCHEMES` config parameter +* [#8342](https://github.com/netbox-community/netbox/issues/8342) - Restore `created` & `last_updated` fields missing from several REST API serializers +* [#8357](https://github.com/netbox-community/netbox/issues/8357) - Add missing tags field to location filter form +* [#8358](https://github.com/netbox-community/netbox/issues/8358) - Fix inconsistent styling of custom fields on filter & bulk edit forms + +--- + ## v3.1.5 (2022-01-06) ### Enhancements diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 470a0b030..42f9d9322 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -100,5 +100,5 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri fields = [ 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', - '_occupied', + '_occupied', 'created', 'last_updated', ] diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 86a55eba5..889792be3 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -22,11 +22,32 @@ CIRCUITTERMINATION_LINK = """ {% endif %} """ +# +# Table columns +# + + +class CommitRateColumn(tables.TemplateColumn): + """ + Humanize the commit rate in the column view + """ + + template_code = """ + {% load helpers %} + {{ record.commit_rate|humanize_speed }} + """ + + def __init__(self, *args, **kwargs): + super().__init__(template_code=self.template_code, *args, **kwargs) + + def value(self, value): + return str(value) if value else None # # Providers # + class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.Column( @@ -45,7 +66,7 @@ class ProviderTable(BaseTable): model = Provider fields = ( 'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', - 'comments', 'tags', + 'comments', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') @@ -69,7 +90,7 @@ class ProviderNetworkTable(BaseTable): class Meta(BaseTable.Meta): model = ProviderNetwork - fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags') + fields = ('pk', 'id', 'name', 'provider', 'description', 'comments', 'tags', 'created', 'last_updated',) default_columns = ('pk', 'name', 'provider', 'description') @@ -92,7 +113,7 @@ class CircuitTypeTable(BaseTable): class Meta(BaseTable.Meta): model = CircuitType - fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions') + fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',) default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') @@ -119,6 +140,7 @@ class CircuitTable(BaseTable): template_code=CIRCUITTERMINATION_LINK, verbose_name='Side Z' ) + commit_rate = CommitRateColumn() comments = MarkdownColumn() tags = TagColumn( url_name='circuits:circuit_list' @@ -128,7 +150,7 @@ class CircuitTable(BaseTable): model = Circuit fields = ( 'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', - 'commit_rate', 'description', 'comments', 'tags', + 'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description', diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 45930c5f5..337fd35c6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -219,7 +219,7 @@ class RackReservationSerializer(PrimaryModelSerializer): class Meta: model = RackReservation fields = [ - 'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', + 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags', 'custom_fields', ] @@ -762,7 +762,7 @@ class CableSerializer(PrimaryModelSerializer): fields = [ 'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', - 'tags', 'custom_fields', + 'tags', 'custom_fields', 'created', 'last_updated', ] def _get_termination(self, obj, side): @@ -856,7 +856,10 @@ class VirtualChassisSerializer(PrimaryModelSerializer): class Meta: model = VirtualChassis - fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count'] + fields = [ + 'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count', + 'created', 'last_updated', + ] # @@ -875,7 +878,10 @@ class PowerPanelSerializer(PrimaryModelSerializer): class Meta: model = PowerPanel - fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count'] + fields = [ + 'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count', + 'created', 'last_updated', + ] class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index fcb37211f..1d3b59497 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -816,6 +816,10 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus' TYPE_FLEXSTACK = 'cisco-flexstack' TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus' + TYPE_STACKWISE80 = 'cisco-stackwise-80' + TYPE_STACKWISE160 = 'cisco-stackwise-160' + TYPE_STACKWISE320 = 'cisco-stackwise-320' + TYPE_STACKWISE480 = 'cisco-stackwise-480' TYPE_JUNIPER_VCP = 'juniper-vcp' TYPE_SUMMITSTACK = 'extreme-summitstack' TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' @@ -950,6 +954,10 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'), (TYPE_FLEXSTACK, 'Cisco FlexStack'), (TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'), + (TYPE_STACKWISE80, 'Cisco StackWise-80'), + (TYPE_STACKWISE160, 'Cisco StackWise-160'), + (TYPE_STACKWISE320, 'Cisco StackWise-320'), + (TYPE_STACKWISE480, 'Cisco StackWise-480'), (TYPE_JUNIPER_VCP, 'Juniper VCP'), (TYPE_SUMMITSTACK, 'Extreme SummitStack'), (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index cb9575d42..f4b4c0a87 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -152,7 +152,7 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): model = Location field_groups = [ - ['q'], + ['q', 'tag'], ['region_id', 'site_group_id', 'site_id', 'parent_id'], ['tenant_group_id', 'tenant_id'], ] diff --git a/netbox/dcim/svg.py b/netbox/dcim/svg.py index e19e8fa2f..1058d8385 100644 --- a/netbox/dcim/svg.py +++ b/netbox/dcim/svg.py @@ -19,7 +19,12 @@ __all__ = ( def get_device_name(device): - return device.name or str(device.device_type) + if device.virtual_chassis: + return f'{device.virtual_chassis.name}:{device.vc_position}' + elif device.name: + return device.name + else: + return str(device.device_type) class RackElevationSVG: diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py index 9b912894b..bea2c0adf 100644 --- a/netbox/dcim/tables/cables.py +++ b/netbox/dcim/tables/cables.py @@ -56,7 +56,7 @@ class CableTable(BaseTable): model = Cable fields = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', - 'status', 'type', 'tenant', 'color', 'length', 'tags', + 'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f0e9c9bb0..5f15d5fbf 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -97,7 +97,7 @@ class DeviceRoleTable(BaseTable): model = DeviceRole fields = ( 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', - 'actions', + 'actions', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') @@ -130,7 +130,7 @@ class PlatformTable(BaseTable): model = Platform fields = ( 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', - 'description', 'tags', 'actions', + 'description', 'tags', 'actions', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', @@ -204,7 +204,8 @@ class DeviceTable(BaseTable): fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4', - 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created', + 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', @@ -297,7 +298,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable): model = ConsolePort fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', - 'link_peer', 'connection', 'tags', + 'link_peer', 'connection', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') @@ -341,7 +342,7 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable): model = ConsoleServerPort fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', - 'cable_color', 'link_peer', 'connection', 'tags', + 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') @@ -386,7 +387,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable): model = PowerPort fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', - 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', + 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') @@ -437,7 +438,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable): model = PowerOutlet fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', - 'cable', 'cable_color', 'link_peer', 'connection', 'tags', + 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description') @@ -515,7 +516,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable 'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', - 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', + 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') @@ -586,7 +587,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable): model = FrontPort fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', - 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', + 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', @@ -637,7 +638,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable): model = RearPort fields = ( 'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable', - 'cable_color', 'link_peer', 'tags', + 'cable_color', 'link_peer', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') @@ -689,7 +690,11 @@ class DeviceBayTable(DeviceComponentTable): class Meta(DeviceComponentTable.Meta): model = DeviceBay - fields = ('pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags') + fields = ( + 'pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags', + 'created', 'last_updated', + ) + default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description') @@ -736,7 +741,7 @@ class InventoryItemTable(DeviceComponentTable): model = InventoryItem fields = ( 'pk', 'id', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', - 'discovered', 'tags', + 'discovered', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag') @@ -788,5 +793,5 @@ class VirtualChassisTable(BaseTable): class Meta(BaseTable.Meta): model = VirtualChassis - fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags') + fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',) default_columns = ('pk', 'name', 'domain', 'master', 'member_count') diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 0aa8ac2bf..dc437ee96 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -50,7 +50,7 @@ class ManufacturerTable(BaseTable): model = Manufacturer fields = ( 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', - 'actions', + 'actions', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions', @@ -84,7 +84,7 @@ class DeviceTypeTable(BaseTable): model = DeviceType fields = ( 'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', - 'airflow', 'comments', 'instance_count', 'tags', + 'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index ac58b64de..c1ea8a34c 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -33,7 +33,7 @@ class PowerPanelTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPanel - fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags') + fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',) default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count') @@ -72,7 +72,7 @@ class PowerFeedTable(CableTerminationTable): fields = ( 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power', - 'comments', 'tags', + 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 14bbe3589..dba28603c 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -31,7 +31,10 @@ class RackRoleTable(BaseTable): class Meta(BaseTable.Meta): model = RackRole - fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions') + fields = ( + 'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', + 'created', 'last_updated', + ) default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') @@ -87,8 +90,9 @@ class RackTable(BaseTable): class Meta(BaseTable.Meta): model = Rack fields = ( - 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', - 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', + 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', + 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', + 'get_power_utilization', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', @@ -127,7 +131,7 @@ class RackReservationTable(BaseTable): model = RackReservation fields = ( 'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags', - 'actions', + 'actions', 'created', 'last_updated', ) default_columns = ( 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions', diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 8ef17c6f2..83e5fa408 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -36,7 +36,7 @@ class RegionTable(BaseTable): class Meta(BaseTable.Meta): model = Region - fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') + fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated') default_columns = ('pk', 'name', 'site_count', 'description', 'actions') @@ -61,7 +61,7 @@ class SiteGroupTable(BaseTable): class Meta(BaseTable.Meta): model = SiteGroup - fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') + fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated') default_columns = ('pk', 'name', 'site_count', 'description', 'actions') @@ -98,7 +98,7 @@ class SiteTable(BaseTable): fields = ( 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', - 'contact_phone', 'contact_email', 'comments', 'tags', + 'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') @@ -138,6 +138,6 @@ class LocationTable(BaseTable): model = Location fields = ( 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', - 'actions', + 'actions', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 9e4665cc2..685949dea 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -61,7 +61,7 @@ class WebhookSerializer(ValidatedModelSerializer): fields = [ 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', - 'conditions', 'ssl_verification', 'ca_file_path', + 'conditions', 'ssl_verification', 'ca_file_path', 'created', 'last_updated', ] @@ -82,7 +82,8 @@ class CustomFieldSerializer(ValidatedModelSerializer): model = CustomField fields = [ 'id', 'url', 'display', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic', - 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', + 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', + 'last_updated', ] @@ -100,7 +101,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): model = CustomLink fields = [ 'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', - 'button_class', 'new_window', + 'button_class', 'new_window', 'created', 'last_updated', ] @@ -118,7 +119,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): model = ExportTemplate fields = [ 'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type', - 'file_extension', 'as_attachment', + 'file_extension', 'as_attachment', 'created', 'last_updated', ] @@ -132,7 +133,9 @@ class TagSerializer(ValidatedModelSerializer): class Meta: model = Tag - fields = ['id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items'] + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tagged_items', 'created', 'last_updated', + ] # diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py index d58e6ce65..bc55d750a 100644 --- a/netbox/extras/forms/customfields.py +++ b/netbox/extras/forms/customfields.py @@ -4,7 +4,7 @@ from django.db.models import Q from extras.choices import * from extras.models import * -from utilities.forms import BootstrapMixin, BulkEditForm, CSVModelForm, FilterForm +from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm __all__ = ( 'CustomFieldModelCSVForm', @@ -34,6 +34,9 @@ class CustomFieldsMixin: raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") return ContentType.objects.get_for_model(self.model) + def _get_custom_fields(self, content_type): + return CustomField.objects.filter(content_types=content_type) + def _get_form_field(self, customfield): return customfield.to_form_field() @@ -41,10 +44,7 @@ class CustomFieldsMixin: """ Append form fields for all CustomFields assigned to this object type. """ - content_type = self._get_content_type() - - # Append form fields; assign initial values if modifying and existing object - for customfield in CustomField.objects.filter(content_types=content_type): + for customfield in self._get_custom_fields(self._get_content_type()): field_name = f'cf_{customfield.name}' self.fields[field_name] = self._get_form_field(customfield) @@ -86,40 +86,37 @@ class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): return customfield.to_form_field(for_csv_import=True) -class CustomFieldModelBulkEditForm(BulkEditForm): +class CustomFieldModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False) - self.custom_fields = [] - self.obj_type = ContentType.objects.get_for_model(self.model) - - # Add all applicable CustomFields to the form - custom_fields = CustomField.objects.filter(content_types=self.obj_type) - for cf in custom_fields: + def _append_customfield_fields(self): + """ + Append form fields for all CustomFields assigned to this object type. + """ + for customfield in self._get_custom_fields(self._get_content_type()): # Annotate non-required custom fields as nullable - if not cf.required: - self.nullable_fields.append(cf.name) - self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) - # Annotate this as a custom field - self.custom_fields.append(cf.name) + if not customfield.required: + self.nullable_fields.append(customfield.name) + + self.fields[customfield.name] = self._get_form_field(customfield) + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(customfield.name) -class CustomFieldModelFilterForm(FilterForm): +class CustomFieldModelFilterForm(BootstrapMixin, CustomFieldsMixin, forms.Form): + q = forms.CharField( + required=False, + label='Search' + ) - def __init__(self, *args, **kwargs): - - self.obj_type = ContentType.objects.get_for_model(self.model) - - super().__init__(*args, **kwargs) - - # Add all applicable CustomFields to the form - self.custom_field_filters = [] - custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude( + def _get_custom_fields(self, content_type): + return CustomField.objects.filter(content_types=content_type).exclude( Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | Q(type=CustomFieldTypeChoices.TYPE_JSON) ) - for cf in custom_fields: - field_name = f'cf_{cf.name}' - self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) - self.custom_field_filters.append(field_name) + + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 89ab7aa19..4f50ba8f4 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -82,7 +82,7 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm): model = ExportTemplate fields = '__all__' fieldsets = ( - ('Custom Link', ('name', 'content_type', 'description')), + ('Export Template', ('name', 'content_type', 'description')), ('Template', ('template_code',)), ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), ) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 8c817ad33..2c2b0ef51 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -16,7 +16,8 @@ from extras.utils import FeatureQuery, extras_features from netbox.models import ChangeLoggedModel from utilities import filters from utilities.forms import ( - CSVChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice, + CSVChoiceField, CSVMultipleChoiceField, DatePicker, LaxURLField, StaticSelectMultiple, StaticSelect, + add_blank_choice, ) from utilities.querysets import RestrictedQuerySet from utilities.validators import validate_regex @@ -238,7 +239,7 @@ class CustomField(ChangeLoggedModel): """ Return a form field suitable for setting a CustomField's value for an object. - set_initial: Set initial date for the field. This should be False when generating a field for bulk editing. + set_initial: Set initial data for the field. This should be False when generating a field for bulk editing. enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing. for_csv_import: Return a form field suitable for bulk import of objects in CSV format. """ @@ -287,7 +288,7 @@ class CustomField(ChangeLoggedModel): choices=choices, required=required, initial=initial, widget=StaticSelect() ) else: - field_class = CSVChoiceField if for_csv_import else forms.MultipleChoiceField + field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField field = field_class( choices=choices, required=required, initial=initial, widget=StaticSelectMultiple() ) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 266f2089a..bee21f697 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -58,7 +58,7 @@ class CustomFieldTable(BaseTable): model = CustomField fields = ( 'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default', - 'description', 'filter_logic', 'choices', + 'description', 'filter_logic', 'choices', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description') @@ -79,7 +79,7 @@ class CustomLinkTable(BaseTable): model = CustomLink fields = ( 'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', - 'button_class', 'new_window', + 'button_class', 'new_window', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window') @@ -100,6 +100,7 @@ class ExportTemplateTable(BaseTable): model = ExportTemplate fields = ( 'pk', 'id', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', + 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', @@ -134,7 +135,7 @@ class WebhookTable(BaseTable): model = Webhook fields = ( 'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', - 'payload_url', 'secret', 'ssl_validation', 'ca_file_path', + 'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', @@ -156,7 +157,7 @@ class TagTable(BaseTable): class Meta(BaseTable.Meta): model = Tag - fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions') + fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions', 'created', 'last_updated',) default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions') @@ -193,7 +194,7 @@ class ConfigContextTable(BaseTable): model = ConfigContext fields = ( 'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', - 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', + 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'weight', 'is_active', 'description') diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index fdabe0fcf..294651c3e 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -122,13 +122,14 @@ class CustomFieldTest(TestCase): def test_select_field(self): obj_type = ContentType.objects.get_for_model(Site) + choices = ['Option A', 'Option B', 'Option C'] # Create a custom field cf = CustomField( type=CustomFieldTypeChoices.TYPE_SELECT, name='my_field', required=False, - choices=['Option A', 'Option B', 'Option C'] + choices=choices ) cf.save() cf.content_types.set([obj_type]) @@ -138,12 +139,47 @@ class CustomFieldTest(TestCase): self.assertIsNone(site.custom_field_data[cf.name]) # Assign a value to the first Site - site.custom_field_data[cf.name] = 'Option A' + site.custom_field_data[cf.name] = choices[0] site.save() # Retrieve the stored value site.refresh_from_db() - self.assertEqual(site.custom_field_data[cf.name], 'Option A') + self.assertEqual(site.custom_field_data[cf.name], choices[0]) + + # Delete the stored value + site.custom_field_data.pop(cf.name) + site.save() + site.refresh_from_db() + self.assertIsNone(site.custom_field_data.get(cf.name)) + + # Delete the custom field + cf.delete() + + def test_multiselect_field(self): + obj_type = ContentType.objects.get_for_model(Site) + choices = ['Option A', 'Option B', 'Option C'] + + # Create a custom field + cf = CustomField( + type=CustomFieldTypeChoices.TYPE_MULTISELECT, + name='my_field', + required=False, + choices=choices + ) + cf.save() + cf.content_types.set([obj_type]) + + # Check that the field has a null initial value + site = Site.objects.first() + self.assertIsNone(site.custom_field_data[cf.name]) + + # Assign a value to the first Site + site.custom_field_data[cf.name] = [choices[0], choices[1]] + site.save() + + # Retrieve the stored value + site.refresh_from_db() + self.assertEqual(site.custom_field_data[cf.name], [choices[0], choices[1]]) # Delete the stored value site.custom_field_data.pop(cf.name) @@ -597,6 +633,9 @@ class CustomFieldImportTest(TestCase): CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[ 'Choice A', 'Choice B', 'Choice C', ]), + CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=[ + 'Choice A', 'Choice B', 'Choice C', + ]), ) for cf in custom_fields: cf.save() @@ -607,19 +646,20 @@ class CustomFieldImportTest(TestCase): Import a Site in CSV format, including a value for each CustomField. """ data = ( - ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select'), - ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A'), - ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B'), - ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', ''), + ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), + ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), + ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), + ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''), ) csv_data = '\n'.join(','.join(row) for row in data) response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data}) self.assertEqual(response.status_code, 200) + self.assertEqual(Site.objects.count(), 3) # Validate data for site 1 site1 = Site.objects.get(name='Site 1') - self.assertEqual(len(site1.custom_field_data), 8) + self.assertEqual(len(site1.custom_field_data), 9) self.assertEqual(site1.custom_field_data['text'], 'ABC') self.assertEqual(site1.custom_field_data['longtext'], 'Foo') self.assertEqual(site1.custom_field_data['integer'], 123) @@ -628,10 +668,11 @@ class CustomFieldImportTest(TestCase): self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1') self.assertEqual(site1.custom_field_data['json'], {"foo": 123}) self.assertEqual(site1.custom_field_data['select'], 'Choice A') + self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B']) # Validate data for site 2 site2 = Site.objects.get(name='Site 2') - self.assertEqual(len(site2.custom_field_data), 8) + self.assertEqual(len(site2.custom_field_data), 9) self.assertEqual(site2.custom_field_data['text'], 'DEF') self.assertEqual(site2.custom_field_data['longtext'], 'Bar') self.assertEqual(site2.custom_field_data['integer'], 456) @@ -640,6 +681,7 @@ class CustomFieldImportTest(TestCase): self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2') self.assertEqual(site2.custom_field_data['json'], {"bar": 456}) self.assertEqual(site2.custom_field_data['select'], 'Choice B') + self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C']) # No custom field data should be set for site 3 site3 = Site.objects.get(name='Site 3') diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 4ed8aa267..08138f4fe 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -592,6 +592,8 @@ class FHRPGroupForm(CustomFieldModelForm): return instance def clean(self): + super().clean() + ip_vrf = self.cleaned_data.get('ip_vrf') ip_address = self.cleaned_data.get('ip_address') ip_status = self.cleaned_data.get('ip_status') diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 9c00a9068..81c3ef34a 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -125,11 +125,30 @@ class ASN(PrimaryModel): verbose_name_plural = 'ASNs' def __str__(self): - return f'AS{self.asn}' + return f'AS{self.asn_with_asdot}' def get_absolute_url(self): return reverse('ipam:asn', args=[self.pk]) + @property + def asn_asdot(self): + """ + Return ASDOT notation for AS numbers greater than 16 bits. + """ + if self.asn > 65535: + return f'{self.asn // 65536}.{self.asn % 65536}' + return self.asn + + @property + def asn_with_asdot(self): + """ + Return both plain and ASDOT notation, where applicable. + """ + if self.asn > 65535: + return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})' + else: + return self.asn + @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Aggregate(GetAvailablePrefixesMixin, PrimaryModel): diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py index 94bc50b93..a8a25f319 100644 --- a/netbox/ipam/tables/fhrp.py +++ b/netbox/ipam/tables/fhrp.py @@ -38,7 +38,7 @@ class FHRPGroupTable(BaseTable): model = FHRPGroup fields = ( 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'interface_count', - 'tags', + 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count') @@ -60,7 +60,7 @@ class FHRPGroupAssignmentTable(BaseTable): ) actions = ButtonsColumn( model=FHRPGroupAssignment, - buttons=('edit', 'delete', 'foo') + buttons=('edit', 'delete') ) class Meta(BaseTable.Meta): diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 3fddbf48e..f188a21c0 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -93,7 +93,10 @@ class RIRTable(BaseTable): class Meta(BaseTable.Meta): model = RIR - fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions') + fields = ( + 'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions', 'created', + 'last_updated', + ) default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') @@ -104,8 +107,10 @@ class RIRTable(BaseTable): class ASNTable(BaseTable): pk = ToggleColumn() asn = tables.Column( + accessor=tables.A('asn_asdot'), linkify=True ) + site_count = LinkedCountColumn( viewname='dcim:site_list', url_params={'asn_id': 'pk'}, @@ -115,7 +120,7 @@ class ASNTable(BaseTable): class Meta(BaseTable.Meta): model = ASN - fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions') + fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions', 'created', 'last_updated',) default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'tenant', 'actions') @@ -147,7 +152,10 @@ class AggregateTable(BaseTable): class Meta(BaseTable.Meta): model = Aggregate - fields = ('pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags') + fields = ( + 'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags', + 'created', 'last_updated', + ) default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') @@ -177,7 +185,10 @@ class RoleTable(BaseTable): class Meta(BaseTable.Meta): model = Role - fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions') + fields = ( + 'pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions', + 'created', 'last_updated', + ) default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') @@ -264,8 +275,8 @@ class PrefixTable(BaseTable): class Meta(BaseTable.Meta): model = Prefix fields = ( - 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan_group', - 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', + 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', + 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', @@ -306,7 +317,7 @@ class IPRangeTable(BaseTable): model = IPRange fields = ( 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', - 'utilization', 'tags', + 'utilization', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', @@ -364,7 +375,7 @@ class IPAddressTable(BaseTable): model = IPAddress fields = ( 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description', - 'tags', + 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description', diff --git a/netbox/ipam/tables/services.py b/netbox/ipam/tables/services.py index ff6b766f7..29039644f 100644 --- a/netbox/ipam/tables/services.py +++ b/netbox/ipam/tables/services.py @@ -31,5 +31,8 @@ class ServiceTable(BaseTable): class Meta(BaseTable.Meta): model = Service - fields = ('pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags') + fields = ( + 'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', 'created', + 'last_updated', + ) default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index 365c6119b..5f38bc868 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -84,7 +84,10 @@ class VLANGroupTable(BaseTable): class Meta(BaseTable.Meta): model = VLANGroup - fields = ('pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions') + fields = ( + 'pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions', + 'created', 'last_updated', + ) default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') @@ -125,7 +128,10 @@ class VLANTable(BaseTable): class Meta(BaseTable.Meta): model = VLAN - fields = ('pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') + fields = ( + 'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags', + 'created', 'last_updated', + ) default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') row_attrs = { 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', diff --git a/netbox/ipam/tables/vrfs.py b/netbox/ipam/tables/vrfs.py index 1264368f4..e71fb1fa4 100644 --- a/netbox/ipam/tables/vrfs.py +++ b/netbox/ipam/tables/vrfs.py @@ -47,7 +47,8 @@ class VRFTable(BaseTable): class Meta(BaseTable.Meta): model = VRF fields = ( - 'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags', + 'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', + 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'rd', 'tenant', 'description') @@ -68,5 +69,5 @@ class RouteTargetTable(BaseTable): class Meta(BaseTable.Meta): model = RouteTarget - fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags') + fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',) default_columns = ('pk', 'name', 'tenant', 'description') diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b3a32a1fc..ff767b4d8 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -19,7 +19,7 @@ from netbox.config import PARAMS # Environment setup # -VERSION = '3.1.5' +VERSION = '3.1.6' # Hostname HOSTNAME = platform.node() diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index a53e70f51..752715e7c 100644 Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 29c3ad3c7..341369adf 100644 Binary files a/netbox/project-static/dist/netbox-light.css and b/netbox/project-static/dist/netbox-light.css differ diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index 23d0be306..61bcedf9c 100644 Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index f8e8c3420..a54b6c324 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -597,6 +597,10 @@ span.color-label { box-shadow: $box-shadow-sm; } +.badge a { + color: inherit; +} + .btn { white-space: nowrap; } diff --git a/netbox/project-static/styles/sidenav.scss b/netbox/project-static/styles/sidenav.scss index 9dfdd855a..4261e5120 100644 --- a/netbox/project-static/styles/sidenav.scss +++ b/netbox/project-static/styles/sidenav.scss @@ -223,11 +223,6 @@ font-weight: $font-weight-bold; color: var(--nbx-sidenav-parent-color); - &.active { - color: $accordion-button-active-color; - background: $accordion-button-active-bg; - } - &:after { display: inline-block; margin-left: auto; @@ -284,7 +279,7 @@ font-size: $font-size-sm; color: var(--nbx-sidenav-link-color); white-space: nowrap; - transition: $transition-100ms-ease-in-out; + transition-duration: 0ms; &.active { background-color: var(--nbx-sidebar-link-active-bg); diff --git a/netbox/project-static/styles/variables.scss b/netbox/project-static/styles/variables.scss index ddeb6025a..8075cf5b0 100644 --- a/netbox/project-static/styles/variables.scss +++ b/netbox/project-static/styles/variables.scss @@ -5,7 +5,7 @@ --nbx-sidebar-bg: #{$gray-200}; --nbx-sidebar-scroll: #{$gray-500}; --nbx-sidebar-link-hover-bg: #{rgba($gray-600, 0.15)}; - --nbx-sidebar-link-active-bg: #{$blue-100}; + --nbx-sidebar-link-active-bg: #9cc8f8; --nbx-sidebar-title-color: #{$text-muted}; --nbx-sidebar-shadow: inset 0px -25px 20px -25px rgba(0, 0, 0, 0.25); --nbx-breadcrumb-bg: #{$light}; diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 65c6651da..f96854ca8 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index 7c56eceac..eb27b4ab0 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 5c46ce3dc..672cb192a 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -42,5 +42,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 814eed25a..816d193de 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 7141191dc..d7f8dff55 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -80,5 +80,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 04c2ebea4..c6452cf78 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -42,5 +42,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index a4517c2e2..19d8298af 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index f1ea82382..82c088392 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index 4a4198c03..868def466 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -45,5 +45,6 @@ {% endblock %} {% block modals %} + {{ block.super }} {% table_config_form table %} {% endblock modals %} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index 1e73fedb2..e6a1e6a28 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -24,17 +24,17 @@ {% else %} {# List all non-customfield filters as declared in the form class #} {% for field in filter_form.visible_fields %} - {% if not filter_form.custom_field_filters or field.name not in filter_form.custom_field_filters %} + {% if not filter_form.custom_fields or field.name not in filter_form.custom_fields %}
{% render_field field %}
{% endif %} {% endfor %} {% endif %} - {% if filter_form.custom_field_filters %} + {% if filter_form.custom_fields %} {# List all custom field filters #}
- {% for name in filter_form.custom_field_filters %} + {% for name in filter_form.custom_fields %}
{% with field=filter_form|get_item:name %} {% render_field field %} diff --git a/netbox/templates/inc/profile_button.html b/netbox/templates/inc/profile_button.html index 230aa02ad..1e562651f 100644 --- a/netbox/templates/inc/profile_button.html +++ b/netbox/templates/inc/profile_button.html @@ -38,7 +38,7 @@
{% else %}
- + Log In