mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-08 08:38:16 -06:00
Merge branch 'feature' into 10500-nested-modules
This commit is contained in:
commit
16e9a9ce6d
@ -86,8 +86,6 @@ CUSTOM_VALIDATORS = {
|
|||||||
|
|
||||||
#### Referencing Related Object Attributes
|
#### Referencing Related Object Attributes
|
||||||
|
|
||||||
!!! info "This feature was introduced in NetBox v4.0."
|
|
||||||
|
|
||||||
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
|
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -104,8 +102,6 @@ CUSTOM_VALIDATORS = {
|
|||||||
|
|
||||||
#### Validating Request Parameters
|
#### Validating Request Parameters
|
||||||
|
|
||||||
!!! info "This feature was introduced in NetBox v4.0."
|
|
||||||
|
|
||||||
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
|
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
# Event Rules
|
# Event Rules
|
||||||
|
|
||||||
NetBox includes the ability to execute certain functions in response to internal object changes. These include:
|
NetBox includes the ability to automatically perform certain functions in response to internal events. These include:
|
||||||
|
|
||||||
* [Scripts](../customization/custom-scripts.md) execution
|
* Executing a [custom script](../customization/custom-scripts.md)
|
||||||
* [Webhooks](../integrations/webhooks.md) execution
|
* Sending a [webhook](../integrations/webhooks.md)
|
||||||
|
* Generating [user notifications](../features/notifications.md)
|
||||||
|
|
||||||
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
|
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
|
||||||
|
|
||||||
|
10
docs/features/notifications.md
Normal file
10
docs/features/notifications.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Notifications
|
||||||
|
|
||||||
|
!!! info "This feature was introduced in NetBox v4.1."
|
||||||
|
|
||||||
|
NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification:
|
||||||
|
|
||||||
|
* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change.
|
||||||
|
* An [event rule](./event-rules.md) can be defined to automatically generate a notification for one or more users in response to specific system events.
|
||||||
|
|
||||||
|
Additionally, NetBox plugins can generate notifications for their own purposes.
|
@ -1,5 +1,7 @@
|
|||||||
# Circuit Groups
|
# Circuit Groups
|
||||||
|
|
||||||
|
!!! info "This feature was introduced in NetBox v4.1."
|
||||||
|
|
||||||
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
|
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
@ -42,4 +42,6 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
|
|||||||
|
|
||||||
### Airflow
|
### Airflow
|
||||||
|
|
||||||
|
!!! info "The `airflow` field was introduced in NetBox v4.1."
|
||||||
|
|
||||||
The direction in which air circulates through the device chassis for cooling.
|
The direction in which air circulates through the device chassis for cooling.
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Rack Types
|
# Rack Types
|
||||||
|
|
||||||
|
!!! info "This feature was introduced in NetBox v4.1."
|
||||||
|
|
||||||
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
|
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
@ -8,9 +10,13 @@ A rack type defines the physical characteristics of a particular model of [rack]
|
|||||||
|
|
||||||
The [manufacturer](./manufacturer.md) which produces this type of rack.
|
The [manufacturer](./manufacturer.md) which produces this type of rack.
|
||||||
|
|
||||||
### Name
|
### Model
|
||||||
|
|
||||||
The unique name of the rack type.
|
The model number assigned to this rack type by its manufacturer. Must be unique to the manufacturer.
|
||||||
|
|
||||||
|
### Slug
|
||||||
|
|
||||||
|
A unique URL-friendly representation of the model identifier. (This value can be used for filtering.)
|
||||||
|
|
||||||
### Form Factor
|
### Form Factor
|
||||||
|
|
||||||
|
@ -44,6 +44,8 @@ For object and multiple-object fields only. Designates the type of NetBox object
|
|||||||
|
|
||||||
### Related Object Filter
|
### Related Object Filter
|
||||||
|
|
||||||
|
!!! info "This field was introduced in NetBox v4.1."
|
||||||
|
|
||||||
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
|
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
@ -18,17 +18,22 @@ The type(s) of object in NetBox that will trigger the rule.
|
|||||||
|
|
||||||
If not selected, the event rule will not be processed.
|
If not selected, the event rule will not be processed.
|
||||||
|
|
||||||
### Events
|
### Events Types
|
||||||
|
|
||||||
The events which will trigger the rule. At least one event type must be selected.
|
The event types which will trigger the rule. At least one event type must be selected.
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|------------|--------------------------------------|
|
|----------------|---------------------------------------------|
|
||||||
| Creations | A new object has been created |
|
| Object created | A new object has been created |
|
||||||
| Updates | An existing object has been modified |
|
| Object updated | An existing object has been modified |
|
||||||
| Deletions | An object has been deleted |
|
| Object deleted | An object has been deleted |
|
||||||
| Job starts | A job for an object starts |
|
| Job started | A background job is initiated |
|
||||||
| Job ends | A job for an object terminates |
|
| Job completed | A background job completes successfully |
|
||||||
|
| Job failed | A background job fails |
|
||||||
|
| Job errored | A background job is aborted due to an error |
|
||||||
|
|
||||||
|
!!! tip "Custom Event Types"
|
||||||
|
The above list includes only built-in event types. NetBox plugins can also register their own custom event types.
|
||||||
|
|
||||||
### Conditions
|
### Conditions
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
|
|||||||
|
|
||||||
### VLAN ID Ranges
|
### VLAN ID Ranges
|
||||||
|
|
||||||
|
!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1."
|
||||||
|
|
||||||
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
|
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
|
@ -50,9 +50,13 @@ The amount of running memory provisioned, in megabytes.
|
|||||||
|
|
||||||
### Disk
|
### Disk
|
||||||
|
|
||||||
The amount of disk storage provisioned, in gigabytes.
|
The amount of disk storage provisioned, in megabytes.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
This field may be directly modified only on virtual machines which do not define discrete [virtual disks](./virtualdisk.md). Otherwise, it will report the sum of all attached disks.
|
||||||
|
|
||||||
### Serial Number
|
### Serial Number
|
||||||
|
|
||||||
Optional serial number assigned to this VM.
|
!!! info "This field was introduced in NetBox v4.1."
|
||||||
|
|
||||||
|
Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.
|
||||||
|
@ -20,6 +20,12 @@ The operational status of the link. Options include:
|
|||||||
|
|
||||||
The service set identifier (SSID) for the wireless link (optional).
|
The service set identifier (SSID) for the wireless link (optional).
|
||||||
|
|
||||||
|
### Distance
|
||||||
|
|
||||||
|
!!! info "This field was introduced in NetBox v4.1."
|
||||||
|
|
||||||
|
The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
|
||||||
|
|
||||||
### Authentication Type
|
### Authentication Type
|
||||||
|
|
||||||
The type of wireless authentication in use. Options include:
|
The type of wireless authentication in use. Options include:
|
||||||
@ -40,7 +46,3 @@ The security cipher used to apply wireless authentication. Options include:
|
|||||||
### Pre-Shared Key
|
### Pre-Shared Key
|
||||||
|
|
||||||
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
||||||
|
|
||||||
### Distance
|
|
||||||
|
|
||||||
The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet).
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Background Jobs
|
# Background Jobs
|
||||||
|
|
||||||
|
!!! info "This feature was introduced in NetBox v4.1."
|
||||||
|
|
||||||
NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
|
NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
|
||||||
|
|
||||||
For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
|
For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
|
||||||
@ -8,12 +10,12 @@ For example, your plugin might need to fetch data from a remote system. Dependin
|
|||||||
|
|
||||||
A background job implements a basic [Job](../../models/core/job.md) executor for all kinds of tasks. It has logic implemented to handle the management of the associated job object, rescheduling of periodic jobs in the given interval and error handling. Adding custom jobs is done by subclassing NetBox's `JobRunner` class.
|
A background job implements a basic [Job](../../models/core/job.md) executor for all kinds of tasks. It has logic implemented to handle the management of the associated job object, rescheduling of periodic jobs in the given interval and error handling. Adding custom jobs is done by subclassing NetBox's `JobRunner` class.
|
||||||
|
|
||||||
::: utilities.jobs.JobRunner
|
::: netbox.jobs.JobRunner
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```python title="jobs.py"
|
```python title="jobs.py"
|
||||||
from utilities.jobs import JobRunner
|
from netbox.jobs import JobRunner
|
||||||
|
|
||||||
|
|
||||||
class MyTestJob(JobRunner):
|
class MyTestJob(JobRunner):
|
||||||
@ -45,7 +47,7 @@ As described above, jobs can be scheduled for immediate execution or at any late
|
|||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```python title="jobs.py"
|
```python title="jobs.py"
|
||||||
from utilities.jobs import JobRunner
|
from netbox.jobs import JobRunner
|
||||||
|
|
||||||
|
|
||||||
class MyHousekeepingJob(JobRunner):
|
class MyHousekeepingJob(JobRunner):
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
# Events
|
# Event Types
|
||||||
|
|
||||||
Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `Event` class. This can be done anywhere within the plugin. An example is provided below.
|
!!! info "This feature was introduced in NetBox v4.1."
|
||||||
|
|
||||||
|
Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from netbox.events import Event, EVENT_TYPE_SUCCESS
|
from netbox.events import EventType, EVENT_TYPE_KIND_SUCCESS
|
||||||
|
|
||||||
Event(
|
EventType(
|
||||||
name='ticket_opened',
|
name='ticket_opened',
|
||||||
text=_('Ticket opened'),
|
text=_('Ticket opened'),
|
||||||
type=EVENT_TYPE_SUCCESS
|
kind=EVENT_TYPE_KIND_SUCCESS
|
||||||
).register()
|
).register()
|
||||||
```
|
```
|
||||||
|
|
||||||
::: netbox.events.Event
|
::: netbox.events.EventType
|
@ -203,7 +203,7 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
|||||||
| `right_page()` | Object view | Inject content on the right side of the page |
|
| `right_page()` | Object view | Inject content on the right side of the page |
|
||||||
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
||||||
|
|
||||||
!!! info "The `navbar()` method was introduced in NetBox v4.1."
|
!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1."
|
||||||
|
|
||||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
|
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
|
||||||
* The unit size for virtual disk size has been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly.
|
* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly.
|
||||||
* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending integer pairs.
|
* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending integer pairs.
|
||||||
|
* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, indicating each assigned event type by name.
|
||||||
* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by #14279).
|
* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by #14279).
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
@ -72,6 +73,9 @@ NetBox now includes a user notification system. Users can subscribe to individua
|
|||||||
* Added the optional `airflow` choice field
|
* Added the optional `airflow` choice field
|
||||||
* extras.CustomField
|
* extras.CustomField
|
||||||
* Added the `related_object_filter` JSON field for object and multi-object custom fields
|
* Added the `related_object_filter` JSON field for object and multi-object custom fields
|
||||||
|
* extras.EventRule
|
||||||
|
* Removed the `type_create`, `type_update`, `type_delete`, `type_job_start`, and `type_job_end` boolean fields
|
||||||
|
* Added the `event_types` array field
|
||||||
* ipam.VLANGroup
|
* ipam.VLANGroup
|
||||||
* Removed the `min_vid` and `max_vid` fields
|
* Removed the `min_vid` and `max_vid` fields
|
||||||
* Added the `vid_ranges` field, and array of starting & ending VLAN IDs
|
* Added the `vid_ranges` field, and array of starting & ending VLAN IDs
|
||||||
|
@ -86,6 +86,7 @@ nav:
|
|||||||
- Change Logging: 'features/change-logging.md'
|
- Change Logging: 'features/change-logging.md'
|
||||||
- Journaling: 'features/journaling.md'
|
- Journaling: 'features/journaling.md'
|
||||||
- Event Rules: 'features/event-rules.md'
|
- Event Rules: 'features/event-rules.md'
|
||||||
|
- Notifications: 'features/notifications.md'
|
||||||
- Background Jobs: 'features/background-jobs.md'
|
- Background Jobs: 'features/background-jobs.md'
|
||||||
- Auth & Permissions: 'features/authentication-permissions.md'
|
- Auth & Permissions: 'features/authentication-permissions.md'
|
||||||
- API & Integration: 'features/api-integration.md'
|
- API & Integration: 'features/api-integration.md'
|
||||||
@ -142,7 +143,7 @@ nav:
|
|||||||
- Forms: 'plugins/development/forms.md'
|
- Forms: 'plugins/development/forms.md'
|
||||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||||
- Search: 'plugins/development/search.md'
|
- Search: 'plugins/development/search.md'
|
||||||
- Events: 'plugins/development/events.md'
|
- Event Types: 'plugins/development/event-types.md'
|
||||||
- Data Backends: 'plugins/development/data-backends.md'
|
- Data Backends: 'plugins/development/data-backends.md'
|
||||||
- REST API: 'plugins/development/rest-api.md'
|
- REST API: 'plugins/development/rest-api.md'
|
||||||
- GraphQL API: 'plugins/development/graphql-api.md'
|
- GraphQL API: 'plugins/development/graphql-api.md'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.events import Event, EVENT_TYPE_DANGER, EVENT_TYPE_SUCCESS, EVENT_TYPE_WARNING
|
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'JOB_COMPLETED',
|
'JOB_COMPLETED',
|
||||||
@ -24,10 +24,10 @@ JOB_FAILED = 'job_failed'
|
|||||||
JOB_ERRORED = 'job_errored'
|
JOB_ERRORED = 'job_errored'
|
||||||
|
|
||||||
# Register core events
|
# Register core events
|
||||||
Event(OBJECT_CREATED, _('Object created')).register()
|
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||||
Event(OBJECT_UPDATED, _('Object updated')).register()
|
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||||
Event(OBJECT_DELETED, _('Object deleted')).register()
|
EventType(OBJECT_DELETED, _('Object deleted')).register()
|
||||||
Event(JOB_STARTED, _('Job started')).register()
|
EventType(JOB_STARTED, _('Job started')).register()
|
||||||
Event(JOB_COMPLETED, _('Job completed'), type=EVENT_TYPE_SUCCESS).register()
|
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||||
Event(JOB_FAILED, _('Job failed'), type=EVENT_TYPE_WARNING).register()
|
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||||
Event(JOB_ERRORED, _('Job errored'), type=EVENT_TYPE_DANGER).register()
|
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -7,6 +6,7 @@ import django_filters
|
|||||||
|
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||||
from netbox.utils import get_data_backend_choices
|
from netbox.utils import get_data_backend_choices
|
||||||
|
from users.models import User
|
||||||
from utilities.filters import ContentTypeFilter
|
from utilities.filters import ContentTypeFilter
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
@ -141,12 +141,12 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User name'),
|
label=_('User name'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.choices import *
|
from core.choices import *
|
||||||
@ -7,6 +6,7 @@ from core.models import *
|
|||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from netbox.forms.mixins import SavedFiltersMixin
|
from netbox.forms.mixins import SavedFiltersMixin
|
||||||
from netbox.utils import get_data_backend_choices
|
from netbox.utils import get_data_backend_choices
|
||||||
|
from users.models import User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
|
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
|
||||||
@ -121,7 +121,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
user = DynamicModelMultipleChoiceField(
|
user = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
@ -150,7 +150,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from netbox.jobs import JobRunner
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from utilities.jobs import JobRunner
|
|
||||||
from .choices import DataSourceStatusChoices
|
from .choices import DataSourceStatusChoices
|
||||||
from .exceptions import SyncError
|
from .exceptions import SyncError
|
||||||
from .models import DataSource
|
from .models import DataSource
|
||||||
|
@ -5,10 +5,10 @@ import sys
|
|||||||
from django import get_version
|
from django import get_version
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Additional objects to include
|
# Additional objects to include
|
||||||
namespace['ObjectType'] = ObjectType
|
namespace['ObjectType'] = ObjectType
|
||||||
namespace['User'] = get_user_model()
|
namespace['User'] = User
|
||||||
|
|
||||||
# Load convenience commands
|
# Load convenience commands
|
||||||
namespace.update({
|
namespace.update({
|
||||||
|
@ -21,7 +21,6 @@ from netbox.registry import registry
|
|||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from ..choices import *
|
from ..choices import *
|
||||||
from ..exceptions import SyncError
|
from ..exceptions import SyncError
|
||||||
from ..signals import post_sync, pre_sync
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AutoSyncRecord',
|
'AutoSyncRecord',
|
||||||
@ -159,6 +158,8 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
||||||
"""
|
"""
|
||||||
|
from core.signals import post_sync, pre_sync
|
||||||
|
|
||||||
if self.status == DataSourceStatusChoices.SYNCING:
|
if self.status == DataSourceStatusChoices.SYNCING:
|
||||||
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ class Job(models.Model):
|
|||||||
management commands only.
|
management commands only.
|
||||||
"""
|
"""
|
||||||
if schedule_at and immediate:
|
if schedule_at and immediate:
|
||||||
raise ValueError("enqueue() cannot be called with values for both schedule_at and immediate.")
|
raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate."))
|
||||||
|
|
||||||
if instance:
|
if instance:
|
||||||
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
||||||
|
@ -7,12 +7,12 @@ from typing import Optional
|
|||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from utilities.datetime import datetime_from_timestamp
|
from utilities.datetime import datetime_from_timestamp
|
||||||
|
|
||||||
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
||||||
|
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -68,16 +68,19 @@ class Plugin:
|
|||||||
installed_version: str = ''
|
installed_version: str = ''
|
||||||
|
|
||||||
|
|
||||||
def get_local_plugins():
|
def get_local_plugins(plugins=None):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of all locally-installed plugins, mapped by name.
|
Return a dictionary of all locally-installed plugins, mapped by name.
|
||||||
"""
|
"""
|
||||||
plugins = {}
|
plugins = plugins or {}
|
||||||
|
local_plugins = {}
|
||||||
|
|
||||||
|
# Gather all locally-installed plugins
|
||||||
for plugin_name in settings.PLUGINS:
|
for plugin_name in settings.PLUGINS:
|
||||||
plugin = importlib.import_module(plugin_name)
|
plugin = importlib.import_module(plugin_name)
|
||||||
plugin_config: PluginConfig = plugin.config
|
plugin_config: PluginConfig = plugin.config
|
||||||
|
|
||||||
plugins[plugin_config.name] = Plugin(
|
local_plugins[plugin_config.name] = Plugin(
|
||||||
slug=plugin_config.name,
|
slug=plugin_config.name,
|
||||||
title_short=plugin_config.verbose_name,
|
title_short=plugin_config.verbose_name,
|
||||||
tag_line=plugin_config.description,
|
tag_line=plugin_config.description,
|
||||||
@ -87,6 +90,14 @@ def get_local_plugins():
|
|||||||
installed_version=plugin_config.version,
|
installed_version=plugin_config.version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||||
|
for k, v in local_plugins.items():
|
||||||
|
if k in plugins:
|
||||||
|
plugins[k].is_local = True
|
||||||
|
plugins[k].is_installed = True
|
||||||
|
else:
|
||||||
|
plugins[k] = v
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
@ -95,7 +106,6 @@ def get_catalog_plugins():
|
|||||||
Return a dictionary of all entries in the plugins catalog, mapped by name.
|
Return a dictionary of all entries in the plugins catalog, mapped by name.
|
||||||
"""
|
"""
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
plugins = {}
|
|
||||||
|
|
||||||
def get_pages():
|
def get_pages():
|
||||||
# TODO: pagination is currently broken in API
|
# TODO: pagination is currently broken in API
|
||||||
@ -121,88 +131,80 @@ def get_catalog_plugins():
|
|||||||
).json()
|
).json()
|
||||||
yield next_page
|
yield next_page
|
||||||
|
|
||||||
for page in get_pages():
|
def make_plugin_dict():
|
||||||
for data in page['data']:
|
plugins = {}
|
||||||
|
|
||||||
# Populate releases
|
for page in get_pages():
|
||||||
releases = []
|
for data in page['data']:
|
||||||
for version in data['release_recent_history']:
|
|
||||||
releases.append(
|
# Populate releases
|
||||||
PluginVersion(
|
releases = []
|
||||||
date=datetime_from_timestamp(version['date']),
|
for version in data['release_recent_history']:
|
||||||
version=version['version'],
|
releases.append(
|
||||||
netbox_min_version=version['netbox_min_version'],
|
PluginVersion(
|
||||||
netbox_max_version=version['netbox_max_version'],
|
date=datetime_from_timestamp(version['date']),
|
||||||
has_model=version['has_model'],
|
version=version['version'],
|
||||||
is_certified=version['is_certified'],
|
netbox_min_version=version['netbox_min_version'],
|
||||||
is_feature=version['is_feature'],
|
netbox_max_version=version['netbox_max_version'],
|
||||||
is_integration=version['is_integration'],
|
has_model=version['has_model'],
|
||||||
is_netboxlabs_supported=version['is_netboxlabs_supported'],
|
is_certified=version['is_certified'],
|
||||||
|
is_feature=version['is_feature'],
|
||||||
|
is_integration=version['is_integration'],
|
||||||
|
is_netboxlabs_supported=version['is_netboxlabs_supported'],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
releases = sorted(releases, key=lambda x: x.date, reverse=True)
|
||||||
|
latest_release = PluginVersion(
|
||||||
|
date=datetime_from_timestamp(data['release_latest']['date']),
|
||||||
|
version=data['release_latest']['version'],
|
||||||
|
netbox_min_version=data['release_latest']['netbox_min_version'],
|
||||||
|
netbox_max_version=data['release_latest']['netbox_max_version'],
|
||||||
|
has_model=data['release_latest']['has_model'],
|
||||||
|
is_certified=data['release_latest']['is_certified'],
|
||||||
|
is_feature=data['release_latest']['is_feature'],
|
||||||
|
is_integration=data['release_latest']['is_integration'],
|
||||||
|
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
|
||||||
)
|
)
|
||||||
releases = sorted(releases, key=lambda x: x.date, reverse=True)
|
|
||||||
latest_release = PluginVersion(
|
|
||||||
date=datetime_from_timestamp(data['release_latest']['date']),
|
|
||||||
version=data['release_latest']['version'],
|
|
||||||
netbox_min_version=data['release_latest']['netbox_min_version'],
|
|
||||||
netbox_max_version=data['release_latest']['netbox_max_version'],
|
|
||||||
has_model=data['release_latest']['has_model'],
|
|
||||||
is_certified=data['release_latest']['is_certified'],
|
|
||||||
is_feature=data['release_latest']['is_feature'],
|
|
||||||
is_integration=data['release_latest']['is_integration'],
|
|
||||||
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Populate author (if any)
|
# Populate author (if any)
|
||||||
if data['author']:
|
if data['author']:
|
||||||
author = PluginAuthor(
|
author = PluginAuthor(
|
||||||
name=data['author']['name'],
|
name=data['author']['name'],
|
||||||
org_id=data['author']['org_id'],
|
org_id=data['author']['org_id'],
|
||||||
url=data['author']['url'],
|
url=data['author']['url'],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
author = None
|
||||||
|
|
||||||
|
# Populate plugin data
|
||||||
|
plugins[data['slug']] = Plugin(
|
||||||
|
id=data['id'],
|
||||||
|
status=data['status'],
|
||||||
|
title_short=data['title_short'],
|
||||||
|
title_long=data['title_long'],
|
||||||
|
tag_line=data['tag_line'],
|
||||||
|
description_short=data['description_short'],
|
||||||
|
slug=data['slug'],
|
||||||
|
author=author,
|
||||||
|
created_at=datetime_from_timestamp(data['created_at']),
|
||||||
|
updated_at=datetime_from_timestamp(data['updated_at']),
|
||||||
|
license_type=data['license_type'],
|
||||||
|
homepage_url=data['homepage_url'],
|
||||||
|
package_name_pypi=data['package_name_pypi'],
|
||||||
|
config_name=data['config_name'],
|
||||||
|
is_certified=data['is_certified'],
|
||||||
|
release_latest=latest_release,
|
||||||
|
release_recent_history=releases,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
author = None
|
|
||||||
|
|
||||||
# Populate plugin data
|
return plugins
|
||||||
plugins[data['slug']] = Plugin(
|
|
||||||
id=data['id'],
|
|
||||||
status=data['status'],
|
|
||||||
title_short=data['title_short'],
|
|
||||||
title_long=data['title_long'],
|
|
||||||
tag_line=data['tag_line'],
|
|
||||||
description_short=data['description_short'],
|
|
||||||
slug=data['slug'],
|
|
||||||
author=author,
|
|
||||||
created_at=datetime_from_timestamp(data['created_at']),
|
|
||||||
updated_at=datetime_from_timestamp(data['updated_at']),
|
|
||||||
license_type=data['license_type'],
|
|
||||||
homepage_url=data['homepage_url'],
|
|
||||||
package_name_pypi=data['package_name_pypi'],
|
|
||||||
config_name=data['config_name'],
|
|
||||||
is_certified=data['is_certified'],
|
|
||||||
release_latest=latest_release,
|
|
||||||
release_recent_history=releases,
|
|
||||||
)
|
|
||||||
|
|
||||||
return plugins
|
catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={})
|
||||||
|
|
||||||
|
|
||||||
def get_plugins():
|
|
||||||
"""
|
|
||||||
Return a dictionary of all plugins (both catalog and locally installed), mapped by name.
|
|
||||||
"""
|
|
||||||
local_plugins = get_local_plugins()
|
|
||||||
catalog_plugins = cache.get('plugins-catalog-feed')
|
|
||||||
if not catalog_plugins:
|
if not catalog_plugins:
|
||||||
catalog_plugins = get_catalog_plugins()
|
try:
|
||||||
cache.set('plugins-catalog-feed', catalog_plugins, 3600)
|
catalog_plugins = make_plugin_dict()
|
||||||
|
cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
plugins = catalog_plugins
|
return catalog_plugins
|
||||||
for k, v in local_plugins.items():
|
|
||||||
if k in plugins:
|
|
||||||
plugins[k].is_local = True
|
|
||||||
plugins[k].is_installed = True
|
|
||||||
else:
|
|
||||||
plugins[k] = v
|
|
||||||
|
|
||||||
return plugins
|
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
from django.db.models.signals import post_save
|
import logging
|
||||||
from django.dispatch import Signal, receiver
|
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||||
|
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||||
|
from django.dispatch import receiver, Signal
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||||
|
|
||||||
|
from core.choices import ObjectChangeActionChoices
|
||||||
|
from core.events import *
|
||||||
|
from core.models import ObjectChange
|
||||||
|
from extras.events import enqueue_event
|
||||||
|
from extras.utils import run_validators
|
||||||
|
from netbox.config import get_config
|
||||||
|
from netbox.context import current_request, events_queue
|
||||||
|
from netbox.models.features import ChangeLoggingMixin
|
||||||
|
from utilities.exceptions import AbortRequest
|
||||||
from .models import ConfigRevision
|
from .models import ConfigRevision
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'clear_events',
|
||||||
'job_end',
|
'job_end',
|
||||||
'job_start',
|
'job_start',
|
||||||
'post_sync',
|
'post_sync',
|
||||||
@ -18,6 +35,152 @@ job_end = Signal()
|
|||||||
pre_sync = Signal()
|
pre_sync = Signal()
|
||||||
post_sync = Signal()
|
post_sync = Signal()
|
||||||
|
|
||||||
|
# Event signals
|
||||||
|
clear_events = Signal()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Change logging & event handling
|
||||||
|
#
|
||||||
|
|
||||||
|
@receiver((post_save, m2m_changed))
|
||||||
|
def handle_changed_object(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Fires when an object is created or updated.
|
||||||
|
"""
|
||||||
|
m2m_changed = False
|
||||||
|
|
||||||
|
if not hasattr(instance, 'to_objectchange'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the current request, or bail if not set
|
||||||
|
request = current_request.get()
|
||||||
|
if request is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine the type of change being made
|
||||||
|
if kwargs.get('created'):
|
||||||
|
event_type = OBJECT_CREATED
|
||||||
|
elif 'created' in kwargs:
|
||||||
|
event_type = OBJECT_UPDATED
|
||||||
|
elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
|
||||||
|
# m2m_changed with objects added or removed
|
||||||
|
m2m_changed = True
|
||||||
|
event_type = OBJECT_UPDATED
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create/update an ObjectChange record for this change
|
||||||
|
action = {
|
||||||
|
OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE,
|
||||||
|
OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE,
|
||||||
|
OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE,
|
||||||
|
}[event_type]
|
||||||
|
objectchange = instance.to_objectchange(action)
|
||||||
|
# If this is a many-to-many field change, check for a previous ObjectChange instance recorded
|
||||||
|
# for this object by this request and update it
|
||||||
|
if m2m_changed and (
|
||||||
|
prev_change := ObjectChange.objects.filter(
|
||||||
|
changed_object_type=ContentType.objects.get_for_model(instance),
|
||||||
|
changed_object_id=instance.pk,
|
||||||
|
request_id=request.id
|
||||||
|
).first()
|
||||||
|
):
|
||||||
|
prev_change.postchange_data = objectchange.postchange_data
|
||||||
|
prev_change.save()
|
||||||
|
elif objectchange and objectchange.has_changes:
|
||||||
|
objectchange.user = request.user
|
||||||
|
objectchange.request_id = request.id
|
||||||
|
objectchange.save()
|
||||||
|
|
||||||
|
# Ensure that we're working with fresh M2M assignments
|
||||||
|
if m2m_changed:
|
||||||
|
instance.refresh_from_db()
|
||||||
|
|
||||||
|
# Enqueue the object for event processing
|
||||||
|
queue = events_queue.get()
|
||||||
|
enqueue_event(queue, instance, request.user, request.id, event_type)
|
||||||
|
events_queue.set(queue)
|
||||||
|
|
||||||
|
# Increment metric counters
|
||||||
|
if event_type == OBJECT_CREATED:
|
||||||
|
model_inserts.labels(instance._meta.model_name).inc()
|
||||||
|
elif event_type == OBJECT_UPDATED:
|
||||||
|
model_updates.labels(instance._meta.model_name).inc()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete)
|
||||||
|
def handle_deleted_object(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Fires when an object is deleted.
|
||||||
|
"""
|
||||||
|
# Run any deletion protection rules for the object. Note that this must occur prior
|
||||||
|
# to queueing any events for the object being deleted, in case a validation error is
|
||||||
|
# raised, causing the deletion to fail.
|
||||||
|
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||||
|
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
||||||
|
try:
|
||||||
|
run_validators(instance, validators)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise AbortRequest(
|
||||||
|
_("Deletion is prevented by a protection rule: {message}").format(message=e)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current request, or bail if not set
|
||||||
|
request = current_request.get()
|
||||||
|
if request is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Record an ObjectChange if applicable
|
||||||
|
if hasattr(instance, 'to_objectchange'):
|
||||||
|
if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
|
||||||
|
instance.snapshot()
|
||||||
|
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||||
|
objectchange.user = request.user
|
||||||
|
objectchange.request_id = request.id
|
||||||
|
objectchange.save()
|
||||||
|
|
||||||
|
# Django does not automatically send an m2m_changed signal for the reverse direction of a
|
||||||
|
# many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
|
||||||
|
# trigger one manually. We do this by checking for any reverse M2M relationships on the
|
||||||
|
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
|
||||||
|
# the association. This triggers an m2m_changed signal with the `post_remove` action type
|
||||||
|
# for the forward direction of the relationship, ensuring that the change is recorded.
|
||||||
|
for relation in instance._meta.related_objects:
|
||||||
|
if type(relation) is not ManyToManyRel:
|
||||||
|
continue
|
||||||
|
related_model = relation.related_model
|
||||||
|
related_field_name = relation.remote_field.name
|
||||||
|
if not issubclass(related_model, ChangeLoggingMixin):
|
||||||
|
# We only care about triggering the m2m_changed signal for models which support
|
||||||
|
# change logging
|
||||||
|
continue
|
||||||
|
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||||
|
obj.snapshot() # Ensure the change record includes the "before" state
|
||||||
|
getattr(obj, related_field_name).remove(instance)
|
||||||
|
|
||||||
|
# Enqueue the object for event processing
|
||||||
|
queue = events_queue.get()
|
||||||
|
enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED)
|
||||||
|
events_queue.set(queue)
|
||||||
|
|
||||||
|
# Increment metric counters
|
||||||
|
model_deletes.labels(instance._meta.model_name).inc()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(clear_events)
|
||||||
|
def clear_events_queue(sender, **kwargs):
|
||||||
|
"""
|
||||||
|
Delete any queued events (e.g. because of an aborted bulk transaction)
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('events')
|
||||||
|
logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
|
||||||
|
events_queue.set({})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# DataSource handlers
|
||||||
|
#
|
||||||
|
|
||||||
@receiver(post_sync)
|
@receiver(post_sync)
|
||||||
def auto_sync(instance, **kwargs):
|
def auto_sync(instance, **kwargs):
|
||||||
|
@ -2,6 +2,7 @@ import json
|
|||||||
import platform
|
import platform
|
||||||
|
|
||||||
from django import __version__ as DJANGO_VERSION
|
from django import __version__ as DJANGO_VERSION
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
@ -37,7 +38,7 @@ from . import filtersets, forms, tables
|
|||||||
from .choices import DataSourceStatusChoices
|
from .choices import DataSourceStatusChoices
|
||||||
from .jobs import SyncDataSourceJob
|
from .jobs import SyncDataSourceJob
|
||||||
from .models import *
|
from .models import *
|
||||||
from .plugins import get_plugins
|
from .plugins import get_catalog_plugins, get_local_plugins
|
||||||
from .tables import CatalogPluginTable, PluginVersionTable
|
from .tables import CatalogPluginTable, PluginVersionTable
|
||||||
|
|
||||||
|
|
||||||
@ -630,10 +631,13 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
|
|
||||||
# Raw data export
|
# Raw data export
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
|
stats['netbox_release'] = stats['netbox_release'].asdict()
|
||||||
|
params = [param.name for param in PARAMS]
|
||||||
data = {
|
data = {
|
||||||
**stats,
|
**stats,
|
||||||
|
'plugins': settings.PLUGINS,
|
||||||
'config': {
|
'config': {
|
||||||
k: config.data[k] for k in sorted(config.data)
|
k: getattr(config, k) for k in sorted(params)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
||||||
@ -650,15 +654,31 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
# Plugins
|
# Plugins
|
||||||
#
|
#
|
||||||
|
|
||||||
class PluginListView(UserPassesTestMixin, View):
|
class BasePluginView(UserPassesTestMixin, View):
|
||||||
|
CACHE_KEY_CATALOG_ERROR = 'plugins-catalog-error'
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
return self.request.user.is_staff
|
return self.request.user.is_staff
|
||||||
|
|
||||||
|
def get_cached_plugins(self, request):
|
||||||
|
catalog_plugins = {}
|
||||||
|
catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False)
|
||||||
|
if not catalog_plugins_error:
|
||||||
|
catalog_plugins = get_catalog_plugins()
|
||||||
|
if not catalog_plugins:
|
||||||
|
# Cache for 5 minutes to avoid spamming connection
|
||||||
|
cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300)
|
||||||
|
messages.warning(request, _("Plugins catalog could not be loaded"))
|
||||||
|
|
||||||
|
return get_local_plugins(catalog_plugins)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginListView(BasePluginView):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
q = request.GET.get('q', None)
|
q = request.GET.get('q', None)
|
||||||
|
|
||||||
plugins = get_plugins().values()
|
plugins = self.get_cached_plugins(request).values()
|
||||||
if q:
|
if q:
|
||||||
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
|
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
|
||||||
|
|
||||||
@ -676,14 +696,11 @@ class PluginListView(UserPassesTestMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class PluginView(UserPassesTestMixin, View):
|
class PluginView(BasePluginView):
|
||||||
|
|
||||||
def test_func(self):
|
|
||||||
return self.request.user.is_staff
|
|
||||||
|
|
||||||
def get(self, request, name):
|
def get(self, request, name):
|
||||||
|
|
||||||
plugins = get_plugins()
|
plugins = self.get_cached_plugins(request)
|
||||||
if name not in plugins:
|
if name not in plugins:
|
||||||
raise Http404(_("Plugin {name} not found").format(name=name))
|
raise Http404(_("Plugin {name} not found").format(name=name))
|
||||||
plugin = plugins[name]
|
plugin = plugins[name]
|
||||||
|
@ -73,12 +73,12 @@ class RackTypeSerializer(RackBaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackType
|
model = RackType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'name', 'slug', 'description', 'form_factor',
|
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
|
||||||
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||||
'max_weight', 'weight_unit', 'mounting_depth', 'airflow', 'description', 'comments', 'tags',
|
'max_weight', 'weight_unit', 'mounting_depth', 'airflow', 'description', 'comments', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'name', 'slug', 'description')
|
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
class RackSerializer(RackBaseSerializer):
|
class RackSerializer(RackBaseSerializer):
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
@ -16,6 +15,7 @@ from netbox.filtersets import (
|
|||||||
)
|
)
|
||||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
|
from users.models import User
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
@ -311,7 +311,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackType
|
model = RackType
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description',
|
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -319,7 +319,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(model__icontains=value) |
|
||||||
Q(description__icontains=value) |
|
Q(description__icontains=value) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
@ -375,6 +375,17 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Location (slug)'),
|
label=_('Location (slug)'),
|
||||||
)
|
)
|
||||||
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='rack_type__manufacturer',
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
label=_('Manufacturer (ID)'),
|
||||||
|
)
|
||||||
|
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='rack_type__manufacturer__slug',
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Manufacturer (slug)'),
|
||||||
|
)
|
||||||
rack_type = django_filters.ModelMultipleChoiceFilter(
|
rack_type = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='rack_type__slug',
|
field_name='rack_type__slug',
|
||||||
queryset=RackType.objects.all(),
|
queryset=RackType.objects.all(),
|
||||||
@ -486,12 +497,12 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
label=_('Location (slug)'),
|
label=_('Location (slug)'),
|
||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
@ -11,6 +10,7 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import ASN, VLAN, VLANGroup, VRF
|
from ipam.models import ASN, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelBulkEditForm
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from users.models import User
|
||||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields
|
from utilities.forms.rendering import FieldSet, InlineFields
|
||||||
@ -459,9 +459,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
queryset=get_user_model().objects.order_by(
|
queryset=User.objects.order_by('username'),
|
||||||
'username'
|
|
||||||
),
|
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tenant = DynamicModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
|
@ -222,7 +222,7 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackType
|
model = RackType
|
||||||
fields = (
|
fields = (
|
||||||
'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||||
'weight_unit', 'description', 'comments', 'tags',
|
'weight_unit', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -10,6 +9,7 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import ASN, VRF
|
from ipam.models import ASN, VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
|
from users.models import User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
@ -312,8 +312,8 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
|||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('status', 'role_id', 'serial', 'asset_tag', name=_('Rack')),
|
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
|
||||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||||
@ -357,6 +357,19 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
|||||||
null_option='None',
|
null_option='None',
|
||||||
label=_('Role')
|
label=_('Role')
|
||||||
)
|
)
|
||||||
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Manufacturer')
|
||||||
|
)
|
||||||
|
rack_type_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=RackType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'manufacturer_id': '$manufacturer_id'
|
||||||
|
},
|
||||||
|
label=_('Rack type')
|
||||||
|
)
|
||||||
serial = forms.CharField(
|
serial = forms.CharField(
|
||||||
label=_('Serial'),
|
label=_('Serial'),
|
||||||
required=False
|
required=False
|
||||||
@ -435,7 +448,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
label=_('Rack')
|
label=_('Rack')
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
@ -11,6 +10,7 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
|
from users.models import User
|
||||||
from utilities.forms import add_blank_choice, get_field_value
|
from utilities.forms import add_blank_choice, get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||||
@ -208,10 +208,13 @@ class RackTypeForm(NetBoxModelForm):
|
|||||||
queryset=Manufacturer.objects.all()
|
queryset=Manufacturer.objects.all()
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
slug = SlugField()
|
slug = SlugField(
|
||||||
|
label=_('Slug'),
|
||||||
|
slug_source='model'
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('manufacturer', 'name', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')),
|
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'width', 'u_height',
|
'width', 'u_height',
|
||||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||||
@ -224,7 +227,7 @@ class RackTypeForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RackType
|
model = RackType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||||
'airflow', 'description', 'comments', 'tags',
|
'airflow', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
@ -311,9 +314,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
queryset=get_user_model().objects.order_by(
|
queryset=User.objects.order_by('username')
|
||||||
'username'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
@ -613,7 +613,6 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
|||||||
filters=RackTypeFilter
|
filters=RackTypeFilter
|
||||||
)
|
)
|
||||||
class RackTypeType(NetBoxObjectType):
|
class RackTypeType(NetBoxObjectType):
|
||||||
_name: str
|
|
||||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,14 +37,7 @@ class Migration(migrations.Migration):
|
|||||||
related_name='rack_types',
|
related_name='rack_types',
|
||||||
to='dcim.manufacturer'
|
to='dcim.manufacturer'
|
||||||
)),
|
)),
|
||||||
('name', models.CharField(max_length=100)),
|
('model', models.CharField(max_length=100)),
|
||||||
('_name', utilities.fields.NaturalOrderingField(
|
|
||||||
'name',
|
|
||||||
blank=True,
|
|
||||||
max_length=100,
|
|
||||||
naturalize_function=utilities.ordering.naturalize
|
|
||||||
),
|
|
||||||
),
|
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
('form_factor', models.CharField(blank=True, max_length=50)),
|
('form_factor', models.CharField(blank=True, max_length=50)),
|
||||||
('width', models.PositiveSmallIntegerField(default=19)),
|
('width', models.PositiveSmallIntegerField(default=19)),
|
||||||
@ -71,7 +64,7 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'verbose_name': 'racktype',
|
'verbose_name': 'racktype',
|
||||||
'verbose_name_plural': 'racktypes',
|
'verbose_name_plural': 'racktypes',
|
||||||
'ordering': ('_name', 'pk'),
|
'ordering': ('manufacturer', 'model'),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
@ -90,4 +83,16 @@ class Migration(migrations.Migration):
|
|||||||
to='dcim.racktype',
|
to='dcim.racktype',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='racktype',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('manufacturer', 'model'), name='dcim_racktype_unique_manufacturer_model'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='racktype',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('manufacturer', 'slug'), name='dcim_racktype_unique_manufacturer_slug'
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -221,7 +221,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_full_name(self):
|
def full_name(self):
|
||||||
return f"{self.manufacturer} {self.model}"
|
return f"{self.manufacturer} {self.model}"
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
@ -417,6 +417,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:moduletype', args=[self.pk])
|
return reverse('dcim:moduletype', args=[self.pk])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.manufacturer} {self.model}"
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = {
|
data = {
|
||||||
'manufacturer': self.manufacturer.name,
|
'manufacturer': self.manufacturer.name,
|
||||||
|
@ -136,15 +136,10 @@ class RackType(RackBase):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='rack_types'
|
related_name='rack_types'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
model = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('model'),
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
@ -152,24 +147,38 @@ class RackType(RackBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
'manufacturer', 'form_factor', 'width', 'u_height', 'airflow', 'desc_units', 'outer_width', 'outer_depth',
|
||||||
'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
'dcim.Manufacturer',
|
'dcim.Manufacturer',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('_name', 'pk') # (site, location, name) may be non-unique
|
ordering = ('manufacturer', 'model')
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('manufacturer', 'model'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_manufacturer_model'
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('manufacturer', 'slug'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_manufacturer_slug'
|
||||||
|
),
|
||||||
|
)
|
||||||
verbose_name = _('rack type')
|
verbose_name = _('rack type')
|
||||||
verbose_name_plural = _('rack types')
|
verbose_name_plural = _('rack types')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.model
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:racktype', args=[self.pk])
|
return reverse('dcim:racktype', args=[self.pk])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.manufacturer} {self.model}"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ class PowerPortIndex(SearchIndex):
|
|||||||
class RackTypeIndex(SearchIndex):
|
class RackTypeIndex(SearchIndex):
|
||||||
model = models.RackType
|
model = models.RackType
|
||||||
fields = (
|
fields = (
|
||||||
('name', 100),
|
('model', 100),
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('comments', 5000),
|
('comments', 5000),
|
||||||
)
|
)
|
||||||
|
@ -50,9 +50,8 @@ class RackRoleTable(NetBoxTable):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackTypeTable(NetBoxTable):
|
class RackTypeTable(NetBoxTable):
|
||||||
name = tables.Column(
|
model = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Model'),
|
||||||
order_by=('_name',),
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
manufacturer = tables.Column(
|
manufacturer = tables.Column(
|
||||||
@ -84,6 +83,11 @@ class RackTypeTable(NetBoxTable):
|
|||||||
comments = columns.MarkdownColumn(
|
comments = columns.MarkdownColumn(
|
||||||
verbose_name=_('Comments'),
|
verbose_name=_('Comments'),
|
||||||
)
|
)
|
||||||
|
instance_count = columns.LinkedCountColumn(
|
||||||
|
viewname='dcim:rack_list',
|
||||||
|
url_params={'rack_type_id': 'pk'},
|
||||||
|
verbose_name=_('Instances')
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:rack_list'
|
url_name='dcim:rack_list'
|
||||||
)
|
)
|
||||||
@ -91,12 +95,12 @@ class RackTypeTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackType
|
model = RackType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
||||||
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments', 'tags',
|
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments',
|
||||||
'created', 'last_updated',
|
'instance_count', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'type', 'u_height', 'description',
|
'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'instance_count',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -124,6 +128,15 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
role = columns.ColoredLabelColumn(
|
role = columns.ColoredLabelColumn(
|
||||||
verbose_name=_('Role'),
|
verbose_name=_('Role'),
|
||||||
)
|
)
|
||||||
|
manufacturer = tables.Column(
|
||||||
|
verbose_name=_('Manufacturer'),
|
||||||
|
accessor=Accessor('rack_type__manufacturer'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
rack_type = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Type')
|
||||||
|
)
|
||||||
u_height = tables.TemplateColumn(
|
u_height = tables.TemplateColumn(
|
||||||
template_code="{{ value }}U",
|
template_code="{{ value }}U",
|
||||||
verbose_name=_('Height')
|
verbose_name=_('Height')
|
||||||
@ -169,14 +182,14 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
|
||||||
'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth',
|
'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
||||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization',
|
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count',
|
||||||
'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'rack_type', 'u_height',
|
||||||
'get_utilization',
|
'device_count', 'get_utilization',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -11,15 +10,13 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from netbox.api.serializers import GenericObjectSerializer
|
from netbox.api.serializers import GenericObjectSerializer
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from users.models import User
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.choices import WirelessChannelChoices
|
from wireless.choices import WirelessChannelChoices
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
@ -276,7 +273,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = RackType
|
model = RackType
|
||||||
brief_fields = ['description', 'display', 'id', 'manufacturer', 'name', 'slug', 'url']
|
brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'new description',
|
'description': 'new description',
|
||||||
}
|
}
|
||||||
@ -290,26 +287,26 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
rack_types = (
|
rack_types = (
|
||||||
RackType(manufacturer=manufacturers[0], name='Rack Type 1', slug='rack-type-1'),
|
RackType(manufacturer=manufacturers[0], model='Rack Type 1', slug='rack-type-1'),
|
||||||
RackType(manufacturer=manufacturers[0], name='Rack Type 2', slug='rack-type-2'),
|
RackType(manufacturer=manufacturers[0], model='Rack Type 2', slug='rack-type-2'),
|
||||||
RackType(manufacturer=manufacturers[0], name='Rack Type 3', slug='rack-type-3'),
|
RackType(manufacturer=manufacturers[0], model='Rack Type 3', slug='rack-type-3'),
|
||||||
)
|
)
|
||||||
RackType.objects.bulk_create(rack_types)
|
RackType.objects.bulk_create(rack_types)
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'manufacturer': manufacturers[1].pk,
|
'manufacturer': manufacturers[1].pk,
|
||||||
'name': 'Rack Type 4',
|
'model': 'Rack Type 4',
|
||||||
'slug': 'rack-type-4',
|
'slug': 'rack-type-4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'manufacturer': manufacturers[1].pk,
|
'manufacturer': manufacturers[1].pk,
|
||||||
'name': 'Rack Type 5',
|
'model': 'Rack Type 5',
|
||||||
'slug': 'rack-type-5',
|
'slug': 'rack-type-5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'manufacturer': manufacturers[1].pk,
|
'manufacturer': manufacturers[1].pk,
|
||||||
'name': 'Rack Type 6',
|
'model': 'Rack Type 6',
|
||||||
'slug': 'rack-type-6',
|
'slug': 'rack-type-6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
@ -8,12 +7,11 @@ from dcim.models import *
|
|||||||
from ipam.models import ASN, IPAddress, RIR, VRF
|
from ipam.models import ASN, IPAddress, RIR, VRF
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from users.models import User
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType, ClusterGroup
|
from virtualization.models import Cluster, ClusterType, ClusterGroup
|
||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSetTests:
|
class DeviceComponentFilterSetTests:
|
||||||
|
|
||||||
@ -484,7 +482,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
racks = (
|
racks = (
|
||||||
RackType(
|
RackType(
|
||||||
manufacturer=manufacturers[0],
|
manufacturer=manufacturers[0],
|
||||||
name='RackType 1',
|
model='RackType 1',
|
||||||
slug='rack-type-1',
|
slug='rack-type-1',
|
||||||
form_factor=RackFormFactorChoices.TYPE_2POST,
|
form_factor=RackFormFactorChoices.TYPE_2POST,
|
||||||
width=RackWidthChoices.WIDTH_19IN,
|
width=RackWidthChoices.WIDTH_19IN,
|
||||||
@ -502,7 +500,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
),
|
),
|
||||||
RackType(
|
RackType(
|
||||||
manufacturer=manufacturers[1],
|
manufacturer=manufacturers[1],
|
||||||
name='RackType 2',
|
model='RackType 2',
|
||||||
slug='rack-type-2',
|
slug='rack-type-2',
|
||||||
form_factor=RackFormFactorChoices.TYPE_4POST,
|
form_factor=RackFormFactorChoices.TYPE_4POST,
|
||||||
width=RackWidthChoices.WIDTH_21IN,
|
width=RackWidthChoices.WIDTH_21IN,
|
||||||
@ -520,7 +518,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
),
|
),
|
||||||
RackType(
|
RackType(
|
||||||
manufacturer=manufacturers[2],
|
manufacturer=manufacturers[2],
|
||||||
name='RackType 3',
|
model='RackType 3',
|
||||||
slug='rack-type-3',
|
slug='rack-type-3',
|
||||||
form_factor=RackFormFactorChoices.TYPE_CABINET,
|
form_factor=RackFormFactorChoices.TYPE_CABINET,
|
||||||
width=RackWidthChoices.WIDTH_23IN,
|
width=RackWidthChoices.WIDTH_23IN,
|
||||||
@ -550,8 +548,8 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_name(self):
|
def test_model(self):
|
||||||
params = {'name': ['RackType 1', 'RackType 2']}
|
params = {'model': ['RackType 1', 'RackType 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_slug(self):
|
def test_slug(self):
|
||||||
@ -653,6 +651,53 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
manufacturers = (
|
||||||
|
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||||
|
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||||
|
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||||
|
)
|
||||||
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
|
rack_types = (
|
||||||
|
RackType(
|
||||||
|
manufacturer=manufacturers[0],
|
||||||
|
model='RackType 1',
|
||||||
|
slug='rack-type-1',
|
||||||
|
form_factor=RackFormFactorChoices.TYPE_2POST,
|
||||||
|
width=RackWidthChoices.WIDTH_19IN,
|
||||||
|
u_height=42,
|
||||||
|
starting_unit=1,
|
||||||
|
desc_units=False,
|
||||||
|
outer_width=100,
|
||||||
|
outer_depth=100,
|
||||||
|
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||||
|
mounting_depth=100,
|
||||||
|
weight=10,
|
||||||
|
max_weight=1000,
|
||||||
|
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||||
|
description='foobar1'
|
||||||
|
),
|
||||||
|
RackType(
|
||||||
|
manufacturer=manufacturers[1],
|
||||||
|
model='RackType 2',
|
||||||
|
slug='rack-type-2',
|
||||||
|
form_factor=RackFormFactorChoices.TYPE_4POST,
|
||||||
|
width=RackWidthChoices.WIDTH_21IN,
|
||||||
|
u_height=43,
|
||||||
|
starting_unit=2,
|
||||||
|
desc_units=False,
|
||||||
|
outer_width=200,
|
||||||
|
outer_depth=200,
|
||||||
|
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||||
|
mounting_depth=200,
|
||||||
|
weight=20,
|
||||||
|
max_weight=2000,
|
||||||
|
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||||
|
description='foobar2'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RackType.objects.bulk_create(rack_types)
|
||||||
|
|
||||||
rack_roles = (
|
rack_roles = (
|
||||||
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
RackRole(name='Rack Role 1', slug='rack-role-1'),
|
||||||
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
RackRole(name='Rack Role 2', slug='rack-role-2'),
|
||||||
@ -742,6 +787,28 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
|
weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
|
||||||
description='foobar3'
|
description='foobar3'
|
||||||
),
|
),
|
||||||
|
Rack(
|
||||||
|
name='Rack 4',
|
||||||
|
facility_id='rack-4',
|
||||||
|
site=sites[2],
|
||||||
|
location=locations[2],
|
||||||
|
tenant=tenants[2],
|
||||||
|
status=RackStatusChoices.STATUS_PLANNED,
|
||||||
|
role=rack_roles[2],
|
||||||
|
rack_type=rack_types[0],
|
||||||
|
description='foobar4'
|
||||||
|
),
|
||||||
|
Rack(
|
||||||
|
name='Rack 5',
|
||||||
|
facility_id='rack-5',
|
||||||
|
site=sites[2],
|
||||||
|
location=locations[2],
|
||||||
|
tenant=tenants[2],
|
||||||
|
status=RackStatusChoices.STATUS_PLANNED,
|
||||||
|
role=rack_roles[2],
|
||||||
|
rack_type=rack_types[1],
|
||||||
|
description='foobar5'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
@ -771,15 +838,15 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
def test_width(self):
|
def test_width(self):
|
||||||
params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]}
|
params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_u_height(self):
|
def test_u_height(self):
|
||||||
params = {'u_height': [42, 43]}
|
params = {'u_height': [42, 43]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_starting_unit(self):
|
def test_starting_unit(self):
|
||||||
params = {'starting_unit': [1]}
|
params = {'starting_unit': [1]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||||
params = {'starting_unit': [2]}
|
params = {'starting_unit': [2]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
|
||||||
@ -787,7 +854,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'desc_units': 'true'}
|
params = {'desc_units': 'true'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
params = {'desc_units': 'false'}
|
params = {'desc_units': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_outer_width(self):
|
def test_outer_width(self):
|
||||||
params = {'outer_width': [100, 200]}
|
params = {'outer_width': [100, 200]}
|
||||||
@ -798,7 +865,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_outer_unit(self):
|
def test_outer_unit(self):
|
||||||
self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3)
|
self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 5)
|
||||||
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
|
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
@ -832,7 +899,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]}
|
params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_role(self):
|
def test_role(self):
|
||||||
roles = RackRole.objects.all()[:2]
|
roles = RackRole.objects.all()[:2]
|
||||||
@ -873,6 +940,20 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_manufacturer(self):
|
||||||
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
|
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack_type(self):
|
||||||
|
rack_types = RackType.objects.all()[:2]
|
||||||
|
params = {'rack_type_id': [rack_types[0].pk, rack_types[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack_type': [rack_types[0].slug, rack_types[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = RackReservation.objects.all()
|
queryset = RackReservation.objects.all()
|
||||||
|
@ -82,7 +82,7 @@ class RackTypeTestCase(TestCase):
|
|||||||
|
|
||||||
RackType.objects.create(
|
RackType.objects.create(
|
||||||
manufacturer=manufacturer,
|
manufacturer=manufacturer,
|
||||||
name='RackType 1',
|
model='RackType 1',
|
||||||
slug='rack-type-1',
|
slug='rack-type-1',
|
||||||
width=11,
|
width=11,
|
||||||
u_height=22,
|
u_height=22,
|
||||||
|
@ -2,7 +2,6 @@ from decimal import Decimal
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
@ -13,11 +12,10 @@ from dcim.models import *
|
|||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from users.models import User
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
|
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = Region
|
model = Region
|
||||||
@ -348,9 +346,9 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
rack_types = (
|
rack_types = (
|
||||||
RackType(manufacturer=manufacturers[0], name='RackType 1', slug='rack-type-1',),
|
RackType(manufacturer=manufacturers[0], model='RackType 1', slug='rack-type-1',),
|
||||||
RackType(manufacturer=manufacturers[0], name='RackType 2', slug='rack-type-2',),
|
RackType(manufacturer=manufacturers[0], model='RackType 2', slug='rack-type-2',),
|
||||||
RackType(manufacturer=manufacturers[0], name='RackType 3', slug='rack-type-3',),
|
RackType(manufacturer=manufacturers[0], model='RackType 3', slug='rack-type-3',),
|
||||||
)
|
)
|
||||||
RackType.objects.bulk_create(rack_types)
|
RackType.objects.bulk_create(rack_types)
|
||||||
|
|
||||||
@ -358,7 +356,7 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'manufacturer': manufacturers[1].pk,
|
'manufacturer': manufacturers[1].pk,
|
||||||
'name': 'RackType X',
|
'model': 'RackType X',
|
||||||
'slug': 'rack-type-x',
|
'slug': 'rack-type-x',
|
||||||
'type': RackFormFactorChoices.TYPE_CABINET,
|
'type': RackFormFactorChoices.TYPE_CABINET,
|
||||||
'width': RackWidthChoices.WIDTH_19IN,
|
'width': RackWidthChoices.WIDTH_19IN,
|
||||||
@ -376,14 +374,14 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"manufacturer,name,slug,width,u_height,weight,max_weight,weight_unit",
|
"manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit",
|
||||||
"Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg",
|
"Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg",
|
||||||
"Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg",
|
"Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg",
|
||||||
"Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg",
|
"Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
"id,name",
|
"id,model",
|
||||||
f"{rack_types[0].pk},RackType 7",
|
f"{rack_types[0].pk},RackType 7",
|
||||||
f"{rack_types[1].pk},RackType 8",
|
f"{rack_types[1].pk},RackType 8",
|
||||||
f"{rack_types[2].pk},RackType 9",
|
f"{rack_types[2].pk},RackType 9",
|
||||||
|
@ -584,7 +584,9 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackTypeListView(generic.ObjectListView):
|
class RackTypeListView(generic.ObjectListView):
|
||||||
queryset = RackType.objects.all()
|
queryset = RackType.objects.annotate(
|
||||||
|
instance_count=count_related(Rack, 'rack_type')
|
||||||
|
)
|
||||||
filterset = filtersets.RackTypeFilterSet
|
filterset = filtersets.RackTypeFilterSet
|
||||||
filterset_form = forms.RackTypeFilterForm
|
filterset_form = forms.RackTypeFilterForm
|
||||||
table = tables.RackTypeTable
|
table = tables.RackTypeTable
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -8,6 +7,7 @@ from extras.choices import *
|
|||||||
from extras.models import JournalEntry
|
from extras.models import JournalEntry
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from users.models import User
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -22,7 +22,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
|||||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
created_by = serializers.PrimaryKeyRelatedField(
|
created_by = serializers.PrimaryKeyRelatedField(
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
default=serializers.CurrentUserDefault()
|
default=serializers.CurrentUserDefault()
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from collections import defaultdict
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
@ -14,6 +12,7 @@ from core.events import *
|
|||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
from users.models import User
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.rqworker import get_rq_retry
|
from utilities.rqworker import get_rq_retry
|
||||||
from utilities.serialization import serialize_object
|
from utilities.serialization import serialize_object
|
||||||
@ -83,7 +82,7 @@ def enqueue_event(queue, instance, user, request_id, event_type):
|
|||||||
|
|
||||||
|
|
||||||
def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request_id=None):
|
def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request_id=None):
|
||||||
user = get_user_model().objects.get(username=username) if username else None
|
user = User.objects.get(username=username) if username else None
|
||||||
|
|
||||||
for event_rule in event_rules:
|
for event_rule in event_rules:
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -284,12 +283,12 @@ class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
field_name='object_types'
|
field_name='object_types'
|
||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -328,12 +327,12 @@ class BookmarkFilterSet(BaseFilterSet):
|
|||||||
object_type_id = MultiValueNumberFilter()
|
object_type_id = MultiValueNumberFilter()
|
||||||
object_type = ContentTypeFilter()
|
object_type = ContentTypeFilter()
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -410,12 +409,12 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
|
|||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='created_by__username',
|
field_name='created_by__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ObjectType, DataFile, DataSource
|
from core.models import ObjectType, DataFile, DataSource
|
||||||
@ -10,7 +9,7 @@ from netbox.events import get_event_type_choices
|
|||||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||||
from netbox.forms.mixins import SavedFiltersMixin
|
from netbox.forms.mixins import SavedFiltersMixin
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.models import Group
|
from users.models import Group, User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||||
@ -453,7 +452,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
|||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
created_by_id = DynamicModelMultipleChoiceField(
|
created_by_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
@ -472,7 +471,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
|||||||
|
|
||||||
class NotificationGroupFilterForm(SavedFiltersMixin, FilterForm):
|
class NotificationGroupFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
|
@ -5,11 +5,11 @@ from contextlib import nullcontext
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.signals import clear_events
|
||||||
from extras.models import Script as ScriptModel
|
from extras.models import Script as ScriptModel
|
||||||
from extras.signals import clear_events
|
|
||||||
from netbox.context_managers import event_tracking
|
from netbox.context_managers import event_tracking
|
||||||
|
from netbox.jobs import JobRunner
|
||||||
from utilities.exceptions import AbortScript, AbortTransaction
|
from utilities.exceptions import AbortScript, AbortTransaction
|
||||||
from utilities.jobs import JobRunner
|
|
||||||
from .utils import is_report
|
from .utils import is_report
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,12 +3,11 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
|
|
||||||
from extras.jobs import ScriptJob
|
from extras.jobs import ScriptJob
|
||||||
from extras.scripts import get_module_and_script
|
from extras.scripts import get_module_and_script
|
||||||
|
from users.models import User
|
||||||
from utilities.request import NetBoxFakeRequest
|
from utilities.request import NetBoxFakeRequest
|
||||||
|
|
||||||
|
|
||||||
@ -28,8 +27,6 @@ class Command(BaseCommand):
|
|||||||
parser.add_argument('script', help="Script to run")
|
parser.add_argument('script', help="Script to run")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
# Params
|
# Params
|
||||||
script = options['script']
|
script = options['script']
|
||||||
loglevel = options['loglevel']
|
loglevel = options['loglevel']
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ __all__ = (
|
|||||||
|
|
||||||
class Dashboard(models.Model):
|
class Dashboard(models.Model):
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
to=get_user_model(),
|
to='users.User',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='dashboard'
|
related_name='dashboard'
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ def get_event_type_choices():
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
(name, event.text)
|
(name, event.text)
|
||||||
for name, event in registry['events'].items()
|
for name, event in registry['event_types'].items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ class Notification(models.Model):
|
|||||||
"""
|
"""
|
||||||
Returns the registered Event which triggered this Notification.
|
Returns the registered Event which triggered this Notification.
|
||||||
"""
|
"""
|
||||||
return registry['events'].get(self.event_type)
|
return registry['event_types'].get(self.event_type)
|
||||||
|
|
||||||
|
|
||||||
class NotificationGroup(ChangeLoggedModel):
|
class NotificationGroup(ChangeLoggedModel):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -34,7 +33,7 @@ class Branch(ChangeLoggedModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=get_user_model(),
|
to='users.User',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
|
@ -1,194 +1,18 @@
|
|||||||
import importlib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||||
from django.dispatch import receiver, Signal
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
|
||||||
|
|
||||||
from core.choices import ObjectChangeActionChoices
|
|
||||||
from core.events import *
|
from core.events import *
|
||||||
from core.models import ObjectChange, ObjectType
|
from core.models import ObjectType
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from extras.events import process_event_rules
|
from extras.events import process_event_rules
|
||||||
from extras.models import EventRule, Notification, Subscription
|
from extras.models import EventRule, Notification, Subscription
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.context import current_request, events_queue
|
|
||||||
from netbox.models.features import ChangeLoggingMixin
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from .events import enqueue_event
|
|
||||||
from .models import CustomField, TaggedItem
|
from .models import CustomField, TaggedItem
|
||||||
from .validators import CustomValidator
|
from .utils import run_validators
|
||||||
|
|
||||||
|
|
||||||
def run_validators(instance, validators):
|
|
||||||
"""
|
|
||||||
Run the provided iterable of validators for the instance.
|
|
||||||
"""
|
|
||||||
request = current_request.get()
|
|
||||||
for validator in validators:
|
|
||||||
|
|
||||||
# Loading a validator class by dotted path
|
|
||||||
if type(validator) is str:
|
|
||||||
module, cls = validator.rsplit('.', 1)
|
|
||||||
validator = getattr(importlib.import_module(module), cls)()
|
|
||||||
|
|
||||||
# Constructing a new instance on the fly from a ruleset
|
|
||||||
elif type(validator) is dict:
|
|
||||||
validator = CustomValidator(validator)
|
|
||||||
|
|
||||||
elif not issubclass(validator.__class__, CustomValidator):
|
|
||||||
raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
|
|
||||||
|
|
||||||
validator(instance, request)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Change logging/webhooks
|
|
||||||
#
|
|
||||||
|
|
||||||
# Define a custom signal that can be sent to clear any queued events
|
|
||||||
clear_events = Signal()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver((post_save, m2m_changed))
|
|
||||||
def handle_changed_object(sender, instance, **kwargs):
|
|
||||||
"""
|
|
||||||
Fires when an object is created or updated.
|
|
||||||
"""
|
|
||||||
m2m_changed = False
|
|
||||||
|
|
||||||
if not hasattr(instance, 'to_objectchange'):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get the current request, or bail if not set
|
|
||||||
request = current_request.get()
|
|
||||||
if request is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine the type of change being made
|
|
||||||
if kwargs.get('created'):
|
|
||||||
event_type = OBJECT_CREATED
|
|
||||||
elif 'created' in kwargs:
|
|
||||||
event_type = OBJECT_UPDATED
|
|
||||||
elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
|
|
||||||
# m2m_changed with objects added or removed
|
|
||||||
m2m_changed = True
|
|
||||||
event_type = OBJECT_UPDATED
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create/update an ObjectChange record for this change
|
|
||||||
action = {
|
|
||||||
OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE,
|
|
||||||
OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE,
|
|
||||||
OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE,
|
|
||||||
}[event_type]
|
|
||||||
objectchange = instance.to_objectchange(action)
|
|
||||||
# If this is a many-to-many field change, check for a previous ObjectChange instance recorded
|
|
||||||
# for this object by this request and update it
|
|
||||||
if m2m_changed and (
|
|
||||||
prev_change := ObjectChange.objects.filter(
|
|
||||||
changed_object_type=ContentType.objects.get_for_model(instance),
|
|
||||||
changed_object_id=instance.pk,
|
|
||||||
request_id=request.id
|
|
||||||
).first()
|
|
||||||
):
|
|
||||||
prev_change.postchange_data = objectchange.postchange_data
|
|
||||||
prev_change.save()
|
|
||||||
elif objectchange and objectchange.has_changes:
|
|
||||||
objectchange.user = request.user
|
|
||||||
objectchange.request_id = request.id
|
|
||||||
objectchange.save()
|
|
||||||
|
|
||||||
# Ensure that we're working with fresh M2M assignments
|
|
||||||
if m2m_changed:
|
|
||||||
instance.refresh_from_db()
|
|
||||||
|
|
||||||
# Enqueue the object for event processing
|
|
||||||
queue = events_queue.get()
|
|
||||||
enqueue_event(queue, instance, request.user, request.id, event_type)
|
|
||||||
events_queue.set(queue)
|
|
||||||
|
|
||||||
# Increment metric counters
|
|
||||||
if event_type == OBJECT_CREATED:
|
|
||||||
model_inserts.labels(instance._meta.model_name).inc()
|
|
||||||
elif event_type == OBJECT_UPDATED:
|
|
||||||
model_updates.labels(instance._meta.model_name).inc()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete)
|
|
||||||
def handle_deleted_object(sender, instance, **kwargs):
|
|
||||||
"""
|
|
||||||
Fires when an object is deleted.
|
|
||||||
"""
|
|
||||||
# Run any deletion protection rules for the object. Note that this must occur prior
|
|
||||||
# to queueing any events for the object being deleted, in case a validation error is
|
|
||||||
# raised, causing the deletion to fail.
|
|
||||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
|
||||||
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
|
||||||
try:
|
|
||||||
run_validators(instance, validators)
|
|
||||||
except ValidationError as e:
|
|
||||||
raise AbortRequest(
|
|
||||||
_("Deletion is prevented by a protection rule: {message}").format(message=e)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the current request, or bail if not set
|
|
||||||
request = current_request.get()
|
|
||||||
if request is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Record an ObjectChange if applicable
|
|
||||||
if hasattr(instance, 'to_objectchange'):
|
|
||||||
if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
|
|
||||||
instance.snapshot()
|
|
||||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
|
||||||
objectchange.user = request.user
|
|
||||||
objectchange.request_id = request.id
|
|
||||||
objectchange.save()
|
|
||||||
|
|
||||||
# Django does not automatically send an m2m_changed signal for the reverse direction of a
|
|
||||||
# many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
|
|
||||||
# trigger one manually. We do this by checking for any reverse M2M relationships on the
|
|
||||||
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
|
|
||||||
# the association. This triggers an m2m_changed signal with the `post_remove` action type
|
|
||||||
# for the forward direction of the relationship, ensuring that the change is recorded.
|
|
||||||
for relation in instance._meta.related_objects:
|
|
||||||
if type(relation) is not ManyToManyRel:
|
|
||||||
continue
|
|
||||||
related_model = relation.related_model
|
|
||||||
related_field_name = relation.remote_field.name
|
|
||||||
if not issubclass(related_model, ChangeLoggingMixin):
|
|
||||||
# We only care about triggering the m2m_changed signal for models which support
|
|
||||||
# change logging
|
|
||||||
continue
|
|
||||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
|
||||||
obj.snapshot() # Ensure the change record includes the "before" state
|
|
||||||
getattr(obj, related_field_name).remove(instance)
|
|
||||||
|
|
||||||
# Enqueue the object for event processing
|
|
||||||
queue = events_queue.get()
|
|
||||||
enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED)
|
|
||||||
events_queue.set(queue)
|
|
||||||
|
|
||||||
# Increment metric counters
|
|
||||||
model_deletes.labels(instance._meta.model_name).inc()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(clear_events)
|
|
||||||
def clear_events_queue(sender, **kwargs):
|
|
||||||
"""
|
|
||||||
Delete any queued events (e.g. because of an aborted bulk transaction)
|
|
||||||
"""
|
|
||||||
logger = logging.getLogger('events')
|
|
||||||
logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
|
|
||||||
events_queue.set({})
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from taggit.managers import _TaggableManager
|
from taggit.managers import _TaggableManager
|
||||||
|
|
||||||
|
from netbox.context import current_request
|
||||||
|
from .validators import CustomValidator
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'image_upload',
|
||||||
|
'is_report',
|
||||||
|
'is_script',
|
||||||
|
'is_taggable',
|
||||||
|
'run_validators',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_taggable(obj):
|
def is_taggable(obj):
|
||||||
"""
|
"""
|
||||||
@ -48,3 +62,25 @@ def is_report(obj):
|
|||||||
return issubclass(obj, Report) and obj != Report
|
return issubclass(obj, Report) and obj != Report
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_validators(instance, validators):
|
||||||
|
"""
|
||||||
|
Run the provided iterable of CustomValidators for the instance.
|
||||||
|
"""
|
||||||
|
request = current_request.get()
|
||||||
|
for validator in validators:
|
||||||
|
|
||||||
|
# Loading a validator class by dotted path
|
||||||
|
if type(validator) is str:
|
||||||
|
module, cls = validator.rsplit('.', 1)
|
||||||
|
validator = getattr(importlib.import_module(module), cls)()
|
||||||
|
|
||||||
|
# Constructing a new instance on the fly from a ruleset
|
||||||
|
elif type(validator) is dict:
|
||||||
|
validator = CustomValidator(validator)
|
||||||
|
|
||||||
|
elif not issubclass(validator.__class__, CustomValidator):
|
||||||
|
raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
|
||||||
|
|
||||||
|
validator(instance, request)
|
||||||
|
@ -551,14 +551,6 @@ class EventRuleListView(generic.ObjectListView):
|
|||||||
class EventRuleView(generic.ObjectView):
|
class EventRuleView(generic.ObjectView):
|
||||||
queryset = EventRule.objects.all()
|
queryset = EventRule.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
return {
|
|
||||||
'event_types': [
|
|
||||||
event for name, event in registry['events'].items()
|
|
||||||
if name in instance.event_types
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(EventRule, 'edit')
|
@register_model_view(EventRule, 'edit')
|
||||||
class EventRuleEditView(generic.ObjectEditView):
|
class EventRuleEditView(generic.ObjectEditView):
|
||||||
|
@ -90,42 +90,45 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def available_vlans_from_range(vlans, vlan_group, vlan_range):
|
def available_vlans_from_range(vlans, vlan_group, vid_range):
|
||||||
"""
|
"""
|
||||||
Create fake records for all gaps between used VLANs
|
Create fake records for all gaps between used VLANs
|
||||||
"""
|
"""
|
||||||
min_vid = int(vlan_range.lower) if vlan_range else VLAN_VID_MIN
|
min_vid = int(vid_range.lower) if vid_range else VLAN_VID_MIN
|
||||||
max_vid = int(vlan_range.upper) if vlan_range else VLAN_VID_MAX
|
max_vid = int(vid_range.upper) if vid_range else VLAN_VID_MAX
|
||||||
|
|
||||||
if not vlans:
|
if not vlans:
|
||||||
return [{
|
return [{
|
||||||
'vid': min_vid,
|
'vid': min_vid,
|
||||||
'vlan_group': vlan_group,
|
'vlan_group': vlan_group,
|
||||||
'available': max_vid - min_vid + 1
|
'available': max_vid - min_vid
|
||||||
}]
|
}]
|
||||||
|
|
||||||
prev_vid = max_vid
|
prev_vid = min_vid - 1
|
||||||
new_vlans = []
|
new_vlans = []
|
||||||
for vlan in vlans:
|
for vlan in vlans:
|
||||||
|
|
||||||
|
# Ignore VIDs outside the range
|
||||||
|
if not min_vid <= vlan.vid < max_vid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Annotate any available VIDs between the previous (or minimum) VID
|
||||||
|
# and the current VID
|
||||||
if vlan.vid - prev_vid > 1:
|
if vlan.vid - prev_vid > 1:
|
||||||
new_vlans.append({
|
new_vlans.append({
|
||||||
'vid': prev_vid + 1,
|
'vid': prev_vid + 1,
|
||||||
'vlan_group': vlan_group,
|
'vlan_group': vlan_group,
|
||||||
'available': vlan.vid - prev_vid - 1,
|
'available': vlan.vid - prev_vid - 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
prev_vid = vlan.vid
|
prev_vid = vlan.vid
|
||||||
|
|
||||||
if vlans[0].vid > min_vid:
|
# Annotate any remaining available VLANs
|
||||||
new_vlans.append({
|
|
||||||
'vid': min_vid,
|
|
||||||
'vlan_group': vlan_group,
|
|
||||||
'available': vlans[0].vid - min_vid,
|
|
||||||
})
|
|
||||||
if prev_vid < max_vid:
|
if prev_vid < max_vid:
|
||||||
new_vlans.append({
|
new_vlans.append({
|
||||||
'vid': prev_vid + 1,
|
'vid': prev_vid + 1,
|
||||||
'vlan_group': vlan_group,
|
'vlan_group': vlan_group,
|
||||||
'available': max_vid - prev_vid,
|
'available': max_vid - prev_vid - 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
return new_vlans
|
return new_vlans
|
||||||
@ -136,8 +139,8 @@ def add_available_vlans(vlans, vlan_group):
|
|||||||
Create fake records for all gaps between used VLANs
|
Create fake records for all gaps between used VLANs
|
||||||
"""
|
"""
|
||||||
new_vlans = []
|
new_vlans = []
|
||||||
for vlan_range in vlan_group.vid_ranges:
|
for vid_range in vlan_group.vid_ranges:
|
||||||
new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vlan_range))
|
new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vid_range))
|
||||||
|
|
||||||
vlans = list(vlans) + new_vlans
|
vlans = list(vlans) + new_vlans
|
||||||
vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid'])
|
vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid'])
|
||||||
|
@ -2,7 +2,6 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
|
from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
@ -10,14 +9,12 @@ from django.db.models import Q
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from users.constants import CONSTRAINT_TOKEN_USER
|
from users.constants import CONSTRAINT_TOKEN_USER
|
||||||
from users.models import Group, ObjectPermission
|
from users.models import Group, ObjectPermission, User
|
||||||
from utilities.permissions import (
|
from utilities.permissions import (
|
||||||
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_type,
|
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_type,
|
||||||
)
|
)
|
||||||
from .misc import _mirror_groups
|
from .misc import _mirror_groups
|
||||||
|
|
||||||
UserModel = get_user_model()
|
|
||||||
|
|
||||||
AUTH_BACKEND_ATTRS = {
|
AUTH_BACKEND_ATTRS = {
|
||||||
# backend name: title, MDI icon name
|
# backend name: title, MDI icon name
|
||||||
'amazon': ('Amazon AWS', 'aws'),
|
'amazon': ('Amazon AWS', 'aws'),
|
||||||
@ -218,15 +215,15 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
# instead we use get_or_create when creating unknown users since it has
|
# instead we use get_or_create when creating unknown users since it has
|
||||||
# built-in safeguards for multiple threads.
|
# built-in safeguards for multiple threads.
|
||||||
if self.create_unknown_user:
|
if self.create_unknown_user:
|
||||||
user, created = UserModel._default_manager.get_or_create(**{
|
user, created = User._default_manager.get_or_create(**{
|
||||||
UserModel.USERNAME_FIELD: username
|
User.USERNAME_FIELD: username
|
||||||
})
|
})
|
||||||
if created:
|
if created:
|
||||||
user = self.configure_user(request, user)
|
user = self.configure_user(request, user)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
user = UserModel._default_manager.get_by_natural_key(username)
|
user = User._default_manager.get_by_natural_key(username)
|
||||||
except UserModel.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
if self.user_can_authenticate(user):
|
if self.user_can_authenticate(user):
|
||||||
if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
|
if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
|
||||||
|
@ -2,41 +2,41 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
EVENT_TYPE_INFO = 'info'
|
EVENT_TYPE_KIND_INFO = 'info'
|
||||||
EVENT_TYPE_SUCCESS = 'success'
|
EVENT_TYPE_KIND_SUCCESS = 'success'
|
||||||
EVENT_TYPE_WARNING = 'warning'
|
EVENT_TYPE_KIND_WARNING = 'warning'
|
||||||
EVENT_TYPE_DANGER = 'danger'
|
EVENT_TYPE_KIND_DANGER = 'danger'
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'EVENT_TYPE_DANGER',
|
'EVENT_TYPE_KIND_DANGER',
|
||||||
'EVENT_TYPE_INFO',
|
'EVENT_TYPE_KIND_INFO',
|
||||||
'EVENT_TYPE_SUCCESS',
|
'EVENT_TYPE_KIND_SUCCESS',
|
||||||
'EVENT_TYPE_WARNING',
|
'EVENT_TYPE_KIND_WARNING',
|
||||||
'Event',
|
'EventType',
|
||||||
'get_event',
|
'get_event_type',
|
||||||
'get_event_type_choices',
|
'get_event_type_choices',
|
||||||
'get_event_text',
|
'get_event_text',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_event(name):
|
def get_event_type(name):
|
||||||
return registry['events'].get(name)
|
return registry['event_types'].get(name)
|
||||||
|
|
||||||
|
|
||||||
def get_event_text(name):
|
def get_event_text(name):
|
||||||
if event := registry['events'].get(name):
|
if event := registry['event_types'].get(name):
|
||||||
return event.text
|
return event.text
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def get_event_type_choices():
|
def get_event_type_choices():
|
||||||
return [
|
return [
|
||||||
(event.name, event.text) for event in registry['events'].values()
|
(event.name, event.text) for event in registry['event_types'].values()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Event:
|
class EventType:
|
||||||
"""
|
"""
|
||||||
A type of event which can occur in NetBox. Event rules can be defined to automatically
|
A type of event which can occur in NetBox. Event rules can be defined to automatically
|
||||||
perform some action in response to an event.
|
perform some action in response to an event.
|
||||||
@ -44,32 +44,32 @@ class Event:
|
|||||||
Args:
|
Args:
|
||||||
name: The unique name under which the event is registered.
|
name: The unique name under which the event is registered.
|
||||||
text: The human-friendly event name. This should support translation.
|
text: The human-friendly event name. This should support translation.
|
||||||
type: The event's classification (info, success, warning, or danger). The default type is info.
|
kind: The event's classification (info, success, warning, or danger). The default type is info.
|
||||||
"""
|
"""
|
||||||
name: str
|
name: str
|
||||||
text: str
|
text: str
|
||||||
type: str = EVENT_TYPE_INFO
|
kind: str = EVENT_TYPE_KIND_INFO
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
if self.name in registry['events']:
|
if self.name in registry['event_types']:
|
||||||
raise Exception(f"An event named {self.name} has already been registered!")
|
raise Exception(f"An event type named {self.name} has already been registered!")
|
||||||
registry['events'][self.name] = self
|
registry['event_types'][self.name] = self
|
||||||
|
|
||||||
def color(self):
|
def color(self):
|
||||||
return {
|
return {
|
||||||
EVENT_TYPE_INFO: 'blue',
|
EVENT_TYPE_KIND_INFO: 'blue',
|
||||||
EVENT_TYPE_SUCCESS: 'green',
|
EVENT_TYPE_KIND_SUCCESS: 'green',
|
||||||
EVENT_TYPE_WARNING: 'orange',
|
EVENT_TYPE_KIND_WARNING: 'orange',
|
||||||
EVENT_TYPE_DANGER: 'red',
|
EVENT_TYPE_KIND_DANGER: 'red',
|
||||||
}.get(self.type)
|
}.get(self.kind)
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return {
|
return {
|
||||||
EVENT_TYPE_INFO: 'mdi mdi-information',
|
EVENT_TYPE_KIND_INFO: 'mdi mdi-information',
|
||||||
EVENT_TYPE_SUCCESS: 'mdi mdi-check-circle',
|
EVENT_TYPE_KIND_SUCCESS: 'mdi mdi-check-circle',
|
||||||
EVENT_TYPE_WARNING: 'mdi mdi-alert-box',
|
EVENT_TYPE_KIND_WARNING: 'mdi mdi-alert-box',
|
||||||
EVENT_TYPE_DANGER: 'mdi mdi-alert-octagon',
|
EVENT_TYPE_KIND_DANGER: 'mdi mdi-alert-octagon',
|
||||||
}.get(self.type)
|
}.get(self.kind)
|
||||||
|
@ -25,7 +25,7 @@ registry = Registry({
|
|||||||
'counter_fields': collections.defaultdict(dict),
|
'counter_fields': collections.defaultdict(dict),
|
||||||
'data_backends': dict(),
|
'data_backends': dict(),
|
||||||
'denormalized_fields': collections.defaultdict(list),
|
'denormalized_fields': collections.defaultdict(list),
|
||||||
'events': dict(),
|
'event_types': dict(),
|
||||||
'model_features': dict(),
|
'model_features': dict(),
|
||||||
'models': collections.defaultdict(set),
|
'models': collections.defaultdict(set),
|
||||||
'plugins': dict(),
|
'plugins': dict(),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -11,14 +10,11 @@ from rest_framework.test import APIClient
|
|||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from ipam.models import Prefix
|
from ipam.models import Prefix
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.testing import TestCase
|
from utilities.testing import TestCase
|
||||||
from utilities.testing.api import APITestCase
|
from utilities.testing.api import APITestCase
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthenticationTestCase(APITestCase):
|
class TokenAuthenticationTestCase(APITestCase):
|
||||||
|
|
||||||
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
@ -8,7 +8,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, Valida
|
|||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
|
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||||
from django.forms import HiddenInput, ModelMultipleChoiceField, MultipleHiddenInput
|
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -17,8 +17,8 @@ from django.utils.translation import gettext as _
|
|||||||
from django_tables2.export import TableExport
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
|
from core.signals import clear_events
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from extras.signals import clear_events
|
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||||
|
@ -13,7 +13,7 @@ from django.utils.html import escape
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.signals import clear_events
|
from core.signals import clear_events
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||||
|
@ -867,13 +867,20 @@ brace-expansion@^1.1.7:
|
|||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
braces@^3.0.2, braces@~3.0.2:
|
braces@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
braces@~3.0.2:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||||
|
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||||
|
dependencies:
|
||||||
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||||
@ -1520,10 +1527,10 @@ file-entry-cache@^6.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache "^3.0.4"
|
flat-cache "^3.0.4"
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
fill-range@^7.0.1, fill-range@^7.1.1:
|
||||||
version "7.0.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
@ -1816,9 +1823,9 @@ ignore@^5.2.0:
|
|||||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||||
|
|
||||||
immutable@^4.0.0:
|
immutable@^4.0.0:
|
||||||
version "4.3.6"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||||
integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==
|
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||||
|
|
||||||
import-fresh@^3.2.1:
|
import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
|
@ -24,7 +24,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock page-header %}
|
{% endblock page-header %}
|
||||||
|
|
||||||
{% block title %}{{ status|capfirst }} {% trans "Workers in " %}{{ queue.name }}{% endblock %}
|
{% block title %}
|
||||||
|
{{ status|capfirst }}
|
||||||
|
{% blocktrans trimmed with queue_name=queue.name %}
|
||||||
|
Workers in {{ queue_name }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block controls %}{% endblock %}
|
{% block controls %}{% endblock %}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Device Type" %}</th>
|
<th scope="row">{% trans "Device Type" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
|
{{ object.device_type|linkify:"full_name" }} ({{ object.device_type.u_height|floatformat }}U)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Module Type" %}</th>
|
<th scope="row">{% trans "Module Type" %}</th>
|
||||||
<td>{{ object.module_type|linkify }}</td>
|
<td>{{ object.module_type|linkify:"full_name" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Status" %}</th>
|
<th scope="row">{% trans "Status" %}</th>
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Rack Type" %}</th>
|
<th scope="row">{% trans "Rack Type" %}</th>
|
||||||
<td>{{ object.rack_type|linkify|placeholder }}</td>
|
<td>{{ object.rack_type|linkify:"full_name"|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Role" %}</th>
|
<th scope="row">{% trans "Role" %}</th>
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
<td>{{ object.manufacturer|linkify }}</td>
|
<td>{{ object.manufacturer|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Name" %}</th>
|
<th scope="row">{% trans "Model" %}</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.model }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Event Types" %}</h5>
|
<h5 class="card-header">{% trans "Event Types" %}</h5>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for name, event in registry.events.items %}
|
{% for name, event in registry.event_types.items %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-07-20 05:02+0000\n"
|
"POT-Creation-Date: 2024-07-27 05:02+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -6955,7 +6955,7 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/model_forms.py:163
|
#: netbox/extras/forms/model_forms.py:163
|
||||||
#: netbox/extras/forms/model_forms.py:204
|
#: netbox/extras/forms/model_forms.py:204
|
||||||
#: netbox/extras/forms/model_forms.py:261
|
#: netbox/extras/forms/model_forms.py:261
|
||||||
#: netbox/extras/forms/model_forms.py:365 netbox/users/forms/model_forms.py:273
|
#: netbox/extras/forms/model_forms.py:365 netbox/users/forms/model_forms.py:277
|
||||||
msgid "Object types"
|
msgid "Object types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -7321,7 +7321,7 @@ msgstr ""
|
|||||||
#: netbox/templates/extras/configcontext.html:60
|
#: netbox/templates/extras/configcontext.html:60
|
||||||
#: netbox/templates/ipam/ipaddress.html:59
|
#: netbox/templates/ipam/ipaddress.html:59
|
||||||
#: netbox/templates/ipam/vlan_edit.html:30
|
#: netbox/templates/ipam/vlan_edit.html:30
|
||||||
#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:311
|
#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:315
|
||||||
msgid "Assignment"
|
msgid "Assignment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -10299,13 +10299,13 @@ msgid "Admin"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:374 netbox/templates/users/group.html:29
|
#: netbox/netbox/navigation/menu.py:374 netbox/templates/users/group.html:29
|
||||||
#: netbox/users/forms/model_forms.py:233 netbox/users/forms/model_forms.py:245
|
#: netbox/users/forms/model_forms.py:237 netbox/users/forms/model_forms.py:249
|
||||||
#: netbox/users/forms/model_forms.py:297 netbox/users/tables.py:102
|
#: netbox/users/forms/model_forms.py:301 netbox/users/tables.py:102
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:394 netbox/users/forms/model_forms.py:182
|
#: netbox/netbox/navigation/menu.py:394 netbox/users/forms/model_forms.py:182
|
||||||
#: netbox/users/forms/model_forms.py:194 netbox/users/forms/model_forms.py:302
|
#: netbox/users/forms/model_forms.py:194 netbox/users/forms/model_forms.py:306
|
||||||
#: netbox/users/tables.py:35 netbox/users/tables.py:106
|
#: netbox/users/tables.py:35 netbox/users/tables.py:106
|
||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -10316,8 +10316,8 @@ msgid "API Tokens"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:421 netbox/users/forms/model_forms.py:188
|
#: netbox/netbox/navigation/menu.py:421 netbox/users/forms/model_forms.py:188
|
||||||
#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:239
|
#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:243
|
||||||
#: netbox/users/forms/model_forms.py:246
|
#: netbox/users/forms/model_forms.py:250
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -12009,7 +12009,7 @@ msgstr ""
|
|||||||
#: netbox/templates/dcim/virtualchassis_add_member.html:27
|
#: netbox/templates/dcim/virtualchassis_add_member.html:27
|
||||||
#: netbox/templates/generic/object_edit.html:78
|
#: netbox/templates/generic/object_edit.html:78
|
||||||
#: netbox/templates/users/objectpermission.html:31
|
#: netbox/templates/users/objectpermission.html:31
|
||||||
#: netbox/users/forms/filtersets.py:68 netbox/users/forms/model_forms.py:309
|
#: netbox/users/forms/filtersets.py:68 netbox/users/forms/model_forms.py:313
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -13104,7 +13104,7 @@ msgid "View"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/users/objectpermission.html:52
|
#: netbox/templates/users/objectpermission.html:52
|
||||||
#: netbox/users/forms/model_forms.py:312
|
#: netbox/users/forms/model_forms.py:316
|
||||||
msgid "Constraints"
|
msgid "Constraints"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -13623,30 +13623,30 @@ msgstr ""
|
|||||||
msgid "Passwords do not match! Please check your input and try again."
|
msgid "Passwords do not match! Please check your input and try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:291
|
#: netbox/users/forms/model_forms.py:295
|
||||||
msgid "Additional actions"
|
msgid "Additional actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:294
|
#: netbox/users/forms/model_forms.py:298
|
||||||
msgid "Actions granted in addition to those listed above"
|
msgid "Actions granted in addition to those listed above"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:310
|
#: netbox/users/forms/model_forms.py:314
|
||||||
msgid "Objects"
|
msgid "Objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:322
|
#: netbox/users/forms/model_forms.py:326
|
||||||
msgid ""
|
msgid ""
|
||||||
"JSON expression of a queryset filter that will return only permitted "
|
"JSON expression of a queryset filter that will return only permitted "
|
||||||
"objects. Leave null to match all objects of this type. A list of multiple "
|
"objects. Leave null to match all objects of this type. A list of multiple "
|
||||||
"objects will result in a logical OR operation."
|
"objects will result in a logical OR operation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:361
|
#: netbox/users/forms/model_forms.py:365
|
||||||
msgid "At least one action must be selected."
|
msgid "At least one action must be selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:379
|
#: netbox/users/forms/model_forms.py:383
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid filter for {model}: {error}"
|
msgid "Invalid filter for {model}: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from drf_spectacular.utils import extend_schema_field
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NestedGroupSerializer',
|
'NestedGroupSerializer',
|
||||||
@ -26,7 +25,7 @@ class NestedGroupSerializer(WritableNestedSerializer):
|
|||||||
class NestedUserSerializer(WritableNestedSerializer):
|
class NestedUserSerializer(WritableNestedSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = get_user_model()
|
model = User
|
||||||
fields = ['id', 'url', 'display_url', 'display', 'username']
|
fields = ['id', 'url', 'display_url', 'display', 'username']
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from django.contrib.auth import get_user_model, password_validation
|
from django.contrib.auth import password_validation
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import SerializedPKRelatedField
|
from netbox.api.fields import SerializedPKRelatedField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
from users.models import Group, ObjectPermission
|
from users.models import Group, ObjectPermission, User
|
||||||
from .permissions import ObjectPermissionSerializer
|
from .permissions import ObjectPermissionSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -49,7 +49,7 @@ class UserSerializer(ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = get_user_model()
|
model = User
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'url', 'display_url', 'display', 'username', 'password', 'first_name', 'last_name', 'email',
|
'id', 'url', 'display_url', 'display', 'username', 'password', 'first_name', 'last_name', 'email',
|
||||||
'is_staff', 'is_active', 'date_joined', 'last_login', 'groups', 'permissions',
|
'is_staff', 'is_active', 'date_joined', 'last_login', 'groups', 'permissions',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
@ -13,7 +12,7 @@ from rest_framework.viewsets import ViewSet
|
|||||||
|
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import Group, ObjectPermission, Token, UserConfig
|
from users.models import Group, ObjectPermission, Token, User, UserConfig
|
||||||
from utilities.data import deepmerge
|
from utilities.data import deepmerge
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -32,7 +31,7 @@ class UsersRootView(APIRootView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class UserViewSet(NetBoxModelViewSet):
|
class UserViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=get_user_model()).order_by('username')
|
queryset = RestrictedQuerySet(model=User).order_by('username')
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
filterset_class = filtersets.UserFilterSet
|
filterset_class = filtersets.UserFilterSet
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from extras.models import NotificationGroup
|
from extras.models import NotificationGroup
|
||||||
from netbox.filtersets import BaseFilterSet
|
from netbox.filtersets import BaseFilterSet
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.filters import ContentTypeFilter
|
from utilities.filters import ContentTypeFilter
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -25,7 +24,7 @@ class GroupFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user',
|
field_name='user',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
permission_id = django_filters.ModelMultipleChoiceFilter(
|
permission_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@ -80,7 +79,7 @@ class UserFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = get_user_model()
|
model = User
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'username', 'first_name', 'last_name', 'email', 'date_joined', 'last_login', 'is_staff', 'is_active',
|
'id', 'username', 'first_name', 'last_name', 'email', 'date_joined', 'last_login', 'is_staff', 'is_active',
|
||||||
'is_superuser',
|
'is_superuser',
|
||||||
@ -104,12 +103,12 @@ class TokenFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user',
|
field_name='user',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -171,12 +170,12 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='users',
|
field_name='users',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='users__username',
|
field_name='users__username',
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
@ -80,7 +79,7 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm):
|
|||||||
label=_('Group')
|
label=_('Group')
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
@ -121,7 +120,7 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
FieldSet('user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')),
|
FieldSet('user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')),
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model, password_validation
|
from django.contrib.auth import password_validation
|
||||||
from django.contrib.postgres.forms import SimpleArrayField
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@ -152,7 +152,7 @@ class UserTokenForm(forms.ModelForm):
|
|||||||
|
|
||||||
class TokenForm(UserTokenForm):
|
class TokenForm(UserTokenForm):
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
queryset=get_user_model().objects.order_by('username'),
|
queryset=User.objects.order_by('username'),
|
||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ class GroupForm(forms.ModelForm):
|
|||||||
users = DynamicModelMultipleChoiceField(
|
users = DynamicModelMultipleChoiceField(
|
||||||
label=_('Users'),
|
label=_('Users'),
|
||||||
required=False,
|
required=False,
|
||||||
queryset=get_user_model().objects.all()
|
queryset=User.objects.all()
|
||||||
)
|
)
|
||||||
object_permissions = DynamicModelMultipleChoiceField(
|
object_permissions = DynamicModelMultipleChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -300,7 +300,7 @@ class ObjectPermissionForm(forms.ModelForm):
|
|||||||
users = DynamicModelMultipleChoiceField(
|
users = DynamicModelMultipleChoiceField(
|
||||||
label=_('Users'),
|
label=_('Users'),
|
||||||
required=False,
|
required=False,
|
||||||
queryset=get_user_model().objects.all()
|
queryset=User.objects.all()
|
||||||
)
|
)
|
||||||
groups = DynamicModelMultipleChoiceField(
|
groups = DynamicModelMultipleChoiceField(
|
||||||
label=_('Groups'),
|
label=_('Groups'),
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import strawberry
|
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from users import filtersets, models
|
|
||||||
|
|
||||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||||
|
from users import filtersets, models
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'GroupFilter',
|
'GroupFilter',
|
||||||
@ -17,7 +15,7 @@ class GroupFilter(BaseFilterMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter(get_user_model(), lookups=True)
|
@strawberry_django.filter(models.User, lookups=True)
|
||||||
@autotype_decorator(filtersets.UserFilterSet)
|
@autotype_decorator(filtersets.UserFilterSet)
|
||||||
class UserFilter(BaseFilterMixin):
|
class UserFilter(BaseFilterMixin):
|
||||||
pass
|
pass
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from users import models
|
from users import models
|
||||||
from .types import *
|
from .types import *
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from netbox.graphql.types import BaseObjectType
|
from netbox.graphql.types import BaseObjectType
|
||||||
from users.models import Group
|
from users.models import Group, User
|
||||||
from .filters import *
|
from .filters import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -23,7 +22,7 @@ class GroupType(BaseObjectType):
|
|||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
get_user_model(),
|
User,
|
||||||
fields=[
|
fields=[
|
||||||
'id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', 'groups',
|
'id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', 'groups',
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ class UserConfig(models.Model):
|
|||||||
This model stores arbitrary user-specific preferences in a JSON data structure.
|
This model stores arbitrary user-specific preferences in a JSON data structure.
|
||||||
"""
|
"""
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
to=get_user_model(),
|
to='users.User',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='config'
|
related_name='config'
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,6 @@ import binascii
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -25,7 +24,7 @@ class Token(models.Model):
|
|||||||
It also supports setting an expiration time and toggling write ability.
|
It also supports setting an expiration time and toggling write ability.
|
||||||
"""
|
"""
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=get_user_model(),
|
to='users.User',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='tokens'
|
related_name='tokens'
|
||||||
)
|
)
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.data import deepmerge
|
from utilities.data import deepmerge
|
||||||
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.testing import BaseFilterSetTests
|
from utilities.testing import BaseFilterSetTests
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class UserTestCase(TestCase, BaseFilterSetTests):
|
class UserTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from users.models import User
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class UserConfigTest(TestCase):
|
class UserConfigTest(TestCase):
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from dcim.tables import SiteTable
|
from dcim.tables import SiteTable
|
||||||
|
from users.models import User
|
||||||
from users.preferences import UserPreference
|
from users.preferences import UserPreference
|
||||||
from utilities.testing import TestCase
|
from utilities.testing import TestCase
|
||||||
|
|
||||||
@ -16,9 +16,6 @@ DEFAULT_USER_PREFERENCES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class UserPreferencesTest(TestCase):
|
class UserPreferencesTest(TestCase):
|
||||||
user_permissions = ['dcim.view_site']
|
user_permissions = ['dcim.view_site']
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
@ -42,6 +42,9 @@ class ReleaseInfo:
|
|||||||
def name(self):
|
def name(self):
|
||||||
return f"NetBox {self.edition} v{self.full_version}"
|
return f"NetBox {self.edition} v{self.full_version}"
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
def load_release_data():
|
def load_release_data():
|
||||||
"""
|
"""
|
||||||
|
@ -3,7 +3,6 @@ import json
|
|||||||
|
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -16,7 +15,7 @@ from strawberry.types.union import StrawberryUnion
|
|||||||
from core.choices import ObjectChangeActionChoices
|
from core.choices import ObjectChangeActionChoices
|
||||||
from core.models import ObjectChange, ObjectType
|
from core.models import ObjectChange, ObjectType
|
||||||
from ipam.graphql.types import IPAddressFamilyType
|
from ipam.graphql.types import IPAddressFamilyType
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import ObjectPermission, Token, User
|
||||||
from utilities.api import get_graphql_type_for_model
|
from utilities.api import get_graphql_type_for_model
|
||||||
from .base import ModelTestCase
|
from .base import ModelTestCase
|
||||||
from .utils import disable_warnings
|
from .utils import disable_warnings
|
||||||
@ -27,9 +26,6 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# REST/GraphQL API Tests
|
# REST/GraphQL API Tests
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField, RangeField
|
from django.contrib.postgres.fields import ArrayField, RangeField
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
@ -11,7 +10,7 @@ from netaddr import IPNetwork
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission, User
|
||||||
from utilities.data import ranges_to_string
|
from utilities.data import ranges_to_string
|
||||||
from utilities.object_types import object_type_identifier
|
from utilities.object_types import object_type_identifier
|
||||||
from utilities.permissions import resolve_permission_type
|
from utilities.permissions import resolve_permission_type
|
||||||
@ -29,7 +28,7 @@ class TestCase(_TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
# Create the test user and assign permissions
|
# Create the test user and assign permissions
|
||||||
self.user = get_user_model().objects.create_user(username='testuser')
|
self.user = User.objects.create_user(username='testuser')
|
||||||
self.add_permissions(*self.user_permissions)
|
self.add_permissions(*self.user_permissions)
|
||||||
|
|
||||||
# Initialize the test client
|
# Initialize the test client
|
||||||
|
@ -3,7 +3,6 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
@ -11,6 +10,7 @@ from core.models import ObjectType
|
|||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, Tag
|
||||||
|
from users.models import User
|
||||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ def create_test_user(username='testuser', permissions=None):
|
|||||||
"""
|
"""
|
||||||
Create a User with the given permissions.
|
Create a User with the given permissions.
|
||||||
"""
|
"""
|
||||||
user = get_user_model().objects.create_user(username=username)
|
user = User.objects.create_user(username=username)
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
permissions = ()
|
permissions = ()
|
||||||
for perm_name in permissions:
|
for perm_name in permissions:
|
||||||
|
Loading…
Reference in New Issue
Block a user