mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Merge branch 'feature' into 8424-device-location
This commit is contained in:
commit
503288ee7e
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -1,5 +1,7 @@
|
|||||||
name: CI
|
name: CI
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
6
.github/workflows/lock.yml
vendored
6
.github/workflows/lock.yml
vendored
@ -4,6 +4,11 @@ name: 'Lock threads'
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * *'
|
- cron: '0 3 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
@ -11,7 +16,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v3
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
|
||||||
issue-inactive-days: 90
|
issue-inactive-days: 90
|
||||||
pr-inactive-days: 30
|
pr-inactive-days: 30
|
||||||
issue-lock-reason: 'resolved'
|
issue-lock-reason: 'resolved'
|
||||||
|
9
.github/workflows/stale.yml
vendored
9
.github/workflows/stale.yml
vendored
@ -1,14 +1,21 @@
|
|||||||
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
|
||||||
name: 'Close stale issues/PRs'
|
name: 'Close stale issues/PRs'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 4 * * *'
|
- cron: '0 4 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v6
|
||||||
with:
|
with:
|
||||||
close-issue-message: >
|
close-issue-message: >
|
||||||
This issue has been automatically closed due to lack of activity. In an
|
This issue has been automatically closed due to lack of activity. In an
|
||||||
|
4
docs/_theme/main.html
vendored
4
docs/_theme/main.html
vendored
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
{% block site_meta %}
|
{% block site_meta %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{# Disable search indexing unless we're building for ReadTheDocs #}
|
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
|
||||||
{% if not config.extra.readthedocs %}
|
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
|
||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,6 +13,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
|
|||||||
* Text: Free-form text (intended for single-line use)
|
* Text: Free-form text (intended for single-line use)
|
||||||
* Long text: Free-form of any length; supports Markdown rendering
|
* Long text: Free-form of any length; supports Markdown rendering
|
||||||
* Integer: A whole number (positive or negative)
|
* Integer: A whole number (positive or negative)
|
||||||
|
* Decimal: A fixed-precision decimal number (4 decimal places)
|
||||||
* Boolean: True or false
|
* Boolean: True or false
|
||||||
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
* Date: A date in ISO 8601 format (YYYY-MM-DD)
|
||||||
* URL: This will be presented as a link in the web UI
|
* URL: This will be presented as a link in the web UI
|
||||||
|
@ -41,6 +41,10 @@ Indicates whether this is a parent type (capable of housing child devices), a ch
|
|||||||
|
|
||||||
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
|
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the device, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||||
|
|
||||||
### Front & Rear Images
|
### Front & Rear Images
|
||||||
|
|
||||||
Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams.
|
Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams.
|
||||||
|
@ -35,3 +35,7 @@ The model number assigned to this module type by its manufacturer. Must be uniqu
|
|||||||
### Part Number
|
### Part Number
|
||||||
|
|
||||||
An alternative part number to uniquely identify the module type.
|
An alternative part number to uniquely identify the module type.
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
|
||||||
|
@ -65,6 +65,10 @@ The height of the rack, measured in units.
|
|||||||
|
|
||||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||||
|
|
||||||
### Descending Units
|
### Descending Units
|
||||||
|
|
||||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
|
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
|
||||||
|
@ -14,6 +14,7 @@ Plugins can do a lot, including:
|
|||||||
* Provide their own "pages" (views) in the web user interface
|
* Provide their own "pages" (views) in the web user interface
|
||||||
* Inject template content and navigation links
|
* Inject template content and navigation links
|
||||||
* Extend NetBox's REST and GraphQL APIs
|
* Extend NetBox's REST and GraphQL APIs
|
||||||
|
* Load additional Django apps
|
||||||
* Add custom request/response middleware
|
* Add custom request/response middleware
|
||||||
|
|
||||||
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
|
||||||
@ -82,6 +83,7 @@ class FooBarConfig(PluginConfig):
|
|||||||
default_settings = {
|
default_settings = {
|
||||||
'baz': True
|
'baz': True
|
||||||
}
|
}
|
||||||
|
django_apps = ["foo", "bar", "baz"]
|
||||||
|
|
||||||
config = FooBarConfig
|
config = FooBarConfig
|
||||||
```
|
```
|
||||||
@ -101,6 +103,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
|
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
|
||||||
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
|
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
|
||||||
| `default_settings` | A dictionary of configuration parameters and their default values |
|
| `default_settings` | A dictionary of configuration parameters and their default values |
|
||||||
|
| `django_apps` | A list of additional Django apps to load alongside the plugin |
|
||||||
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
|
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
|
||||||
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
|
||||||
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
|
||||||
@ -112,6 +115,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||||||
|
|
||||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||||
|
|
||||||
|
#### Important Notes About `django_apps`
|
||||||
|
|
||||||
|
Loading additional apps may cause more harm than good and could make identifying problems within NetBox itself more difficult. The `django_apps` attribute is intended only for advanced use cases that require a deeper Django integration.
|
||||||
|
|
||||||
|
Apps from this list are inserted *before* the plugin's `PluginConfig` in the order defined. Adding the plugin's `PluginConfig` module to this list changes this behavior and allows for apps to be loaded *after* the plugin.
|
||||||
|
|
||||||
|
Any additional apps must be installed within the same Python environment as NetBox or `ImproperlyConfigured` exceptions will be raised when loading the plugin.
|
||||||
|
|
||||||
## Create setup.py
|
## Create setup.py
|
||||||
|
|
||||||
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:
|
||||||
|
@ -2,17 +2,27 @@
|
|||||||
|
|
||||||
## v3.3.5 (FUTURE)
|
## v3.3.5 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
|
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
|
||||||
|
* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
|
||||||
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
|
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
|
||||||
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
|
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
|
||||||
|
* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
|
||||||
|
* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
|
||||||
|
* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
|
||||||
|
* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.3.4 (2022-09-16)
|
## v3.3.4 (2022-09-16)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
|
* [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
|
||||||
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
|
* [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
|
||||||
|
|
||||||
|
@ -18,11 +18,14 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
||||||
|
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
||||||
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
|
* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups
|
||||||
|
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
|
||||||
|
|
||||||
### Plugins API
|
### Plugins API
|
||||||
|
|
||||||
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
|
* [#9071](https://github.com/netbox-community/netbox/issues/9071) - Introduce `PluginMenu` for top-level plugin navigation menus
|
||||||
|
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
|
||||||
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
|
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
@ -35,5 +38,11 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
|
|
||||||
* circuits.provider
|
* circuits.provider
|
||||||
* Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields
|
* Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields
|
||||||
|
* dcim.DeviceType
|
||||||
|
* Added optional `weight` and `weight_unit` fields
|
||||||
|
* dcim.ModuleType
|
||||||
|
* Added optional `weight` and `weight_unit` fields
|
||||||
|
* dcim.Rack
|
||||||
|
* Added optional `weight` and `weight_unit` fields
|
||||||
* ipam.FHRPGroup
|
* ipam.FHRPGroup
|
||||||
* Added optional `name` field
|
* Added optional `name` field
|
||||||
|
@ -38,7 +38,6 @@ plugins:
|
|||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
show_source: false
|
show_source: false
|
||||||
extra:
|
extra:
|
||||||
readthedocs: !ENV READTHEDOCS
|
|
||||||
social:
|
social:
|
||||||
- icon: fontawesome/brands/github
|
- icon: fontawesome/brands/github
|
||||||
link: https://github.com/netbox-community/netbox
|
link: https://github.com/netbox-community/netbox
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import dcim.fields
|
import dcim.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('cid', models.CharField(max_length=100)),
|
('cid', models.CharField(max_length=100)),
|
||||||
('status', models.CharField(default='active', max_length=50)),
|
('status', models.CharField(default='active', max_length=50)),
|
||||||
@ -58,7 +58,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -73,7 +73,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -93,7 +93,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('description', models.CharField(blank=True, max_length=200)),
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='circuittermination',
|
model_name='circuittermination',
|
||||||
name='custom_field_data',
|
name='custom_field_data',
|
||||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='circuittermination',
|
model_name='circuittermination',
|
||||||
|
@ -201,6 +201,7 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
default=None)
|
default=None)
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -208,8 +209,9 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
||||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
|
||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
'outer_depth', 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
|
'powerfeed_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -315,27 +317,29 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
|||||||
)
|
)
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'subdevice_role', 'airflow', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||||
'last_updated', 'device_count',
|
'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
# module_count = serializers.IntegerField(read_only=True)
|
# module_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
|
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1314,6 +1314,24 @@ class CableLengthUnitChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WeightUnitChoices(ChoiceSet):
|
||||||
|
|
||||||
|
# Metric
|
||||||
|
UNIT_KILOGRAM = 'kg'
|
||||||
|
UNIT_GRAM = 'g'
|
||||||
|
|
||||||
|
# Imperial
|
||||||
|
UNIT_POUND = 'lb'
|
||||||
|
UNIT_OUNCE = 'oz'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(UNIT_KILOGRAM, 'Kilograms'),
|
||||||
|
(UNIT_GRAM, 'Grams'),
|
||||||
|
(UNIT_POUND, 'Pounds'),
|
||||||
|
(UNIT_OUNCE, 'Ounces'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# CableTerminations
|
# CableTerminations
|
||||||
#
|
#
|
||||||
|
@ -320,7 +320,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||||
'outer_unit',
|
'outer_unit', 'weight', 'weight_unit'
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -482,7 +482,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -576,7 +576,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = ['id', 'model', 'part_number']
|
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -285,15 +285,26 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
widget=SmallTextarea,
|
widget=SmallTextarea,
|
||||||
label='Comments'
|
label='Comments'
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
|
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
|
||||||
('Location', ('region', 'site_group', 'site', 'location')),
|
('Location', ('region', 'site_group', 'site', 'location')),
|
||||||
('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
|
('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'weight', 'weight_unit'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -355,12 +366,23 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
|
('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('part_number', 'airflow')
|
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit')
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
@ -371,12 +393,23 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
part_number = forms.CharField(
|
part_number = forms.CharField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'part_number')),
|
('Module Type', ('manufacturer', 'part_number')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('part_number',)
|
nullable_fields = ('part_number', 'weight', 'weight_unit')
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
@ -553,17 +586,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
'type', 'status', 'tenant', 'label', 'color', 'length',
|
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Validate length/unit
|
|
||||||
length = self.cleaned_data.get('length')
|
|
||||||
length_unit = self.cleaned_data.get('length_unit')
|
|
||||||
if length and not length_unit:
|
|
||||||
raise forms.ValidationError({
|
|
||||||
'length_unit': "Must specify a unit when setting length"
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
domain = forms.CharField(
|
domain = forms.CharField(
|
||||||
|
@ -228,6 +228,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -281,6 +282,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackElevationFilterForm(RackFilterForm):
|
class RackElevationFilterForm(RackFilterForm):
|
||||||
@ -370,6 +378,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
||||||
)),
|
)),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -465,6 +474,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||||
@ -476,6 +492,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||||
'pass_through_ports',
|
'pass_through_ports',
|
||||||
)),
|
)),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -529,6 +546,13 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
@ -260,7 +260,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
|
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
|
||||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||||
'outer_unit', 'comments', 'tags',
|
'outer_unit', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
@ -273,6 +273,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
|||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
'width': StaticSelect(),
|
'width': StaticSelect(),
|
||||||
'outer_unit': StaticSelect(),
|
'outer_unit': StaticSelect(),
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -363,6 +364,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
('Chassis', (
|
('Chassis', (
|
||||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||||
)),
|
)),
|
||||||
|
('Attributes', ('weight', 'weight_unit')),
|
||||||
('Images', ('front_image', 'rear_image')),
|
('Images', ('front_image', 'rear_image')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -370,7 +372,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||||
'front_image', 'rear_image', 'comments', 'tags',
|
'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'airflow': StaticSelect(),
|
'airflow': StaticSelect(),
|
||||||
@ -380,7 +382,8 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
}),
|
}),
|
||||||
'rear_image': ClearableFileInput(attrs={
|
'rear_image': ClearableFileInput(attrs={
|
||||||
'accept': DEVICETYPE_IMAGE_FORMATS
|
'accept': DEVICETYPE_IMAGE_FORMATS
|
||||||
})
|
}),
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -392,16 +395,20 @@ class ModuleTypeForm(NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Module Type', (
|
('Module Type', (
|
||||||
'manufacturer', 'model', 'part_number', 'tags',
|
'manufacturer', 'model', 'part_number', 'tags', 'weight', 'weight_unit'
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'part_number', 'comments', 'tags',
|
'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleForm(NetBoxModelForm):
|
class DeviceRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
@ -211,6 +211,9 @@ class DeviceTypeType(NetBoxObjectType):
|
|||||||
def resolve_airflow(self, info):
|
def resolve_airflow(self, info):
|
||||||
return self.airflow or None
|
return self.airflow or None
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class FrontPortType(ComponentObjectType, CabledObjectMixin):
|
class FrontPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
@ -328,6 +331,9 @@ class ModuleTypeType(NetBoxObjectType):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
filterset_class = filtersets.ModuleTypeFilterSet
|
filterset_class = filtersets.ModuleTypeFilterSet
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class PlatformType(OrganizationalObjectType):
|
class PlatformType(OrganizationalObjectType):
|
||||||
|
|
||||||
@ -416,6 +422,9 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
|||||||
def resolve_outer_unit(self, info):
|
def resolve_outer_unit(self, info):
|
||||||
return self.outer_unit or None
|
return self.outer_unit or None
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class RackReservationType(NetBoxObjectType):
|
class RackReservationType(NetBoxObjectType):
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import dcim.fields
|
import dcim.fields
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -28,7 +28,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('termination_a_id', models.PositiveIntegerField()),
|
('termination_a_id', models.PositiveIntegerField()),
|
||||||
('termination_b_id', models.PositiveIntegerField()),
|
('termination_b_id', models.PositiveIntegerField()),
|
||||||
@ -60,7 +60,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -96,7 +96,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -132,7 +132,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('local_context_data', models.JSONField(blank=True, null=True)),
|
('local_context_data', models.JSONField(blank=True, null=True)),
|
||||||
('name', models.CharField(blank=True, max_length=64, null=True)),
|
('name', models.CharField(blank=True, max_length=64, null=True)),
|
||||||
@ -155,7 +155,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -186,7 +186,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -203,7 +203,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('model', models.CharField(max_length=100)),
|
('model', models.CharField(max_length=100)),
|
||||||
('slug', models.SlugField(max_length=100)),
|
('slug', models.SlugField(max_length=100)),
|
||||||
@ -224,7 +224,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -261,7 +261,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('label', models.CharField(blank=True, max_length=64)),
|
('label', models.CharField(blank=True, max_length=64)),
|
||||||
@ -302,7 +302,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -326,7 +326,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('slug', models.SlugField(max_length=100)),
|
('slug', models.SlugField(max_length=100)),
|
||||||
@ -345,7 +345,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -360,7 +360,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -377,7 +377,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('_cable_peer_id', models.PositiveIntegerField(blank=True, null=True)),
|
('_cable_peer_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
('mark_connected', models.BooleanField(default=False)),
|
('mark_connected', models.BooleanField(default=False)),
|
||||||
@ -401,7 +401,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -438,7 +438,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
],
|
],
|
||||||
@ -451,7 +451,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -490,7 +490,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -516,7 +516,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||||
('description', models.CharField(max_length=200)),
|
('description', models.CharField(max_length=200)),
|
||||||
@ -530,7 +530,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -546,7 +546,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -583,7 +583,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -602,7 +602,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -630,7 +630,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -649,7 +649,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('domain', models.CharField(blank=True, max_length=30)),
|
('domain', models.CharField(blank=True, max_length=30)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
@ -107,7 +107,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('model', models.CharField(max_length=100)),
|
('model', models.CharField(max_length=100)),
|
||||||
('part_number', models.CharField(blank=True, max_length=50)),
|
('part_number', models.CharField(blank=True, max_length=50)),
|
||||||
@ -125,7 +125,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
|
||||||
@ -145,7 +145,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('local_context_data', models.JSONField(blank=True, null=True)),
|
('local_context_data', models.JSONField(blank=True, null=True)),
|
||||||
('serial', models.CharField(blank=True, max_length=50)),
|
('serial', models.CharField(blank=True, max_length=50)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 4.0.7 on 2022-09-23 01:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0162_unique_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
]
|
@ -1,7 +1,8 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -21,6 +22,7 @@ from netbox.models import OrganizationalModel, NetBoxModel
|
|||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from .device_components import *
|
from .device_components import *
|
||||||
|
from .mixins import WeightMixin
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -71,7 +73,7 @@ class Manufacturer(OrganizationalModel):
|
|||||||
return reverse('dcim:manufacturer', args=[self.pk])
|
return reverse('dcim:manufacturer', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(NetBoxModel):
|
class DeviceType(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||||
well as high-level functional role(s).
|
well as high-level functional role(s).
|
||||||
@ -139,7 +141,7 @@ class DeviceType(NetBoxModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -315,7 +317,7 @@ class DeviceType(NetBoxModel):
|
|||||||
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
||||||
|
|
||||||
|
|
||||||
class ModuleType(NetBoxModel):
|
class ModuleType(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
||||||
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
||||||
@ -344,7 +346,7 @@ class ModuleType(NetBoxModel):
|
|||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('manufacturer',)
|
clone_fields = ('manufacturer', 'weight', 'weight_unit',)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('manufacturer', 'model')
|
ordering = ('manufacturer', 'model')
|
||||||
@ -946,6 +948,18 @@ class Device(NetBoxModel, ConfigContextModel):
|
|||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return DeviceStatusChoices.colors.get(self.status)
|
return DeviceStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def total_weight(self):
|
||||||
|
total_weight = sum(
|
||||||
|
module.module_type._abs_weight
|
||||||
|
for module in Module.objects.filter(device=self)
|
||||||
|
.exclude(module_type___abs_weight__isnull=True)
|
||||||
|
.prefetch_related('module_type')
|
||||||
|
)
|
||||||
|
if self.device_type._abs_weight:
|
||||||
|
total_weight += self.device_type._abs_weight
|
||||||
|
return round(total_weight / 1000, 2)
|
||||||
|
|
||||||
|
|
||||||
class Module(NetBoxModel, ConfigContextModel):
|
class Module(NetBoxModel, ConfigContextModel):
|
||||||
"""
|
"""
|
||||||
|
45
netbox/dcim/models/mixins.py
Normal file
45
netbox/dcim/models/mixins.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from dcim.choices import *
|
||||||
|
from utilities.utils import to_grams
|
||||||
|
|
||||||
|
|
||||||
|
class WeightMixin(models.Model):
|
||||||
|
weight = models.DecimalField(
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
weight_unit = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
# Stores the normalized weight (in grams) for database ordering
|
||||||
|
_abs_weight = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Store the given weight (if any) in grams for use in database ordering
|
||||||
|
if self.weight and self.weight_unit:
|
||||||
|
self._abs_weight = to_grams(self.weight, self.weight_unit)
|
||||||
|
else:
|
||||||
|
self._abs_weight = None
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Validate weight and weight_unit
|
||||||
|
if self.weight is not None and not self.weight_unit:
|
||||||
|
raise ValidationError("Must specify a unit when setting a weight")
|
||||||
|
elif self.weight is None:
|
||||||
|
self.weight_unit = ''
|
@ -1,4 +1,5 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -18,7 +19,8 @@ from utilities.choices import ColorChoices
|
|||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.utils import array_to_string, drange
|
from utilities.utils import array_to_string, drange
|
||||||
from .device_components import PowerPort
|
from .device_components import PowerPort
|
||||||
from .devices import Device
|
from .devices import Device, Module
|
||||||
|
from .mixins import WeightMixin
|
||||||
from .power import PowerFeed
|
from .power import PowerFeed
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -62,7 +64,7 @@ class RackRole(OrganizationalModel):
|
|||||||
return reverse('dcim:rackrole', args=[self.pk])
|
return reverse('dcim:rackrole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
class Rack(NetBoxModel):
|
class Rack(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||||
Each Rack is assigned to a Site and (optionally) a Location.
|
Each Rack is assigned to a Site and (optionally) a Location.
|
||||||
@ -185,7 +187,7 @@ class Rack(NetBoxModel):
|
|||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||||
'outer_depth', 'outer_unit',
|
'outer_depth', 'outer_unit', 'weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -454,6 +456,22 @@ class Rack(NetBoxModel):
|
|||||||
|
|
||||||
return int(allocated_draw / available_power_total * 100)
|
return int(allocated_draw / available_power_total * 100)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def total_weight(self):
|
||||||
|
total_weight = sum(
|
||||||
|
device.device_type._abs_weight
|
||||||
|
for device in self.devices.exclude(device_type___abs_weight__isnull=True).prefetch_related('device_type')
|
||||||
|
)
|
||||||
|
total_weight += sum(
|
||||||
|
module.module_type._abs_weight
|
||||||
|
for module in Module.objects.filter(device__rack=self)
|
||||||
|
.exclude(module_type___abs_weight__isnull=True)
|
||||||
|
.prefetch_related('module_type')
|
||||||
|
)
|
||||||
|
if self._abs_weight:
|
||||||
|
total_weight += self._abs_weight
|
||||||
|
return round(total_weight / 1000, 2)
|
||||||
|
|
||||||
|
|
||||||
class RackReservation(NetBoxModel):
|
class RackReservation(NetBoxModel):
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,7 @@ class Node(Hyperlink):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, position, width, url, color, labels, radius=10, **extra):
|
def __init__(self, position, width, url, color, labels, radius=10, **extra):
|
||||||
super(Node, self).__init__(href=url, target='_blank', **extra)
|
super(Node, self).__init__(href=url, target='_parent', **extra)
|
||||||
|
|
||||||
x, y = position
|
x, y = position
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from svgwrite.text import Text
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.template.defaultfilters import floatformat
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ def get_device_description(device):
|
|||||||
device.device_role,
|
device.device_role,
|
||||||
device.device_type.manufacturer.name,
|
device.device_type.manufacturer.name,
|
||||||
device.device_type.model,
|
device.device_type.model,
|
||||||
device.device_type.u_height,
|
floatformat(device.device_type.u_height),
|
||||||
device.asset_tag or '',
|
device.asset_tag or '',
|
||||||
device.serial or ''
|
device.serial or ''
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@ from dcim.models import (
|
|||||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
)
|
)
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConsolePortTemplateTable',
|
'ConsolePortTemplateTable',
|
||||||
@ -85,12 +85,22 @@ class DeviceTypeTable(NetBoxTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicetype_list'
|
url_name='dcim:devicetype_list'
|
||||||
)
|
)
|
||||||
|
u_height = columns.TemplateColumn(
|
||||||
|
template_code='{{ value|floatformat }}'
|
||||||
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
u_height = columns.TemplateColumn(
|
||||||
|
template_code='{{ value|floatformat }}'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||||
'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
'airflow', 'weight', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||||
|
@ -2,6 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from dcim.models import Module, ModuleType
|
from dcim.models import Module, ModuleType
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
from .template_code import DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ModuleTable',
|
'ModuleTable',
|
||||||
@ -26,11 +27,15 @@ class ModuleTypeTable(NetBoxTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:moduletype_list'
|
url_name='dcim:moduletype_list'
|
||||||
)
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number',
|
'pk', 'model', 'manufacturer', 'part_number',
|
||||||
|
@ -4,6 +4,7 @@ from django_tables2.utils import Accessor
|
|||||||
from dcim.models import Rack, RackReservation, RackRole
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
|
from .template_code import DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RackTable',
|
'RackTable',
|
||||||
@ -82,13 +83,17 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
||||||
verbose_name='Outer Depth'
|
verbose_name='Outer Depth'
|
||||||
)
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
|
||||||
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', 'asset_tag',
|
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
||||||
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
'asset_tag', 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'weight', 'comments',
|
||||||
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
|
'device_count', 'get_utilization', 'get_power_utilization', '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', 'u_height', 'device_count',
|
||||||
|
@ -15,6 +15,11 @@ CABLE_LENGTH = """
|
|||||||
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEVICE_WEIGHT = """
|
||||||
|
{% load helpers %}
|
||||||
|
{% if record.weight %}{{ record.weight|simplify_decimal }} {{ record.weight_unit }}{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
DEVICE_LINK = """
|
DEVICE_LINK = """
|
||||||
<a href="{% url 'dcim:device' pk=record.pk %}">
|
<a href="{% url 'dcim:device' pk=record.pk %}">
|
||||||
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}
|
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}
|
||||||
|
@ -409,9 +409,9 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
racks = (
|
racks = (
|
||||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
@ -517,6 +517,14 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [10, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight_unit(self):
|
||||||
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
|
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()
|
||||||
@ -688,9 +696,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
device_types = (
|
device_types = (
|
||||||
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png'),
|
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR),
|
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT),
|
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
DeviceType.objects.bulk_create(device_types)
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
@ -839,6 +847,14 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'inventory_items': 'false'}
|
params = {'inventory_items': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [10, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight_unit(self):
|
||||||
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ModuleType.objects.all()
|
queryset = ModuleType.objects.all()
|
||||||
@ -855,9 +871,9 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
module_types = (
|
module_types = (
|
||||||
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1'),
|
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2'),
|
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2', weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3'),
|
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3', weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
ModuleType.objects.bulk_create(module_types)
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
@ -943,6 +959,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'pass_through_ports': 'false'}
|
params = {'pass_through_ports': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [10, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight_unit(self):
|
||||||
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ConsolePortTemplate.objects.all()
|
queryset = ConsolePortTemplate.objects.all()
|
||||||
|
@ -99,6 +99,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
types = CustomFieldTypeChoices
|
types = CustomFieldTypeChoices
|
||||||
if obj.type == types.TYPE_INTEGER:
|
if obj.type == types.TYPE_INTEGER:
|
||||||
return 'integer'
|
return 'integer'
|
||||||
|
if obj.type == types.TYPE_DECIMAL:
|
||||||
|
return 'decimal'
|
||||||
if obj.type == types.TYPE_BOOLEAN:
|
if obj.type == types.TYPE_BOOLEAN:
|
||||||
return 'boolean'
|
return 'boolean'
|
||||||
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
||||||
|
@ -10,6 +10,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
|||||||
TYPE_TEXT = 'text'
|
TYPE_TEXT = 'text'
|
||||||
TYPE_LONGTEXT = 'longtext'
|
TYPE_LONGTEXT = 'longtext'
|
||||||
TYPE_INTEGER = 'integer'
|
TYPE_INTEGER = 'integer'
|
||||||
|
TYPE_DECIMAL = 'decimal'
|
||||||
TYPE_BOOLEAN = 'boolean'
|
TYPE_BOOLEAN = 'boolean'
|
||||||
TYPE_DATE = 'date'
|
TYPE_DATE = 'date'
|
||||||
TYPE_URL = 'url'
|
TYPE_URL = 'url'
|
||||||
@ -23,6 +24,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
|||||||
(TYPE_TEXT, 'Text'),
|
(TYPE_TEXT, 'Text'),
|
||||||
(TYPE_LONGTEXT, 'Text (long)'),
|
(TYPE_LONGTEXT, 'Text (long)'),
|
||||||
(TYPE_INTEGER, 'Integer'),
|
(TYPE_INTEGER, 'Integer'),
|
||||||
|
(TYPE_DECIMAL, 'Decimal'),
|
||||||
(TYPE_BOOLEAN, 'Boolean (true/false)'),
|
(TYPE_BOOLEAN, 'Boolean (true/false)'),
|
||||||
(TYPE_DATE, 'Date'),
|
(TYPE_DATE, 'Date'),
|
||||||
(TYPE_URL, 'URL'),
|
(TYPE_URL, 'URL'),
|
||||||
|
@ -34,7 +34,9 @@ class CustomFieldsMixin:
|
|||||||
return ContentType.objects.get_for_model(self.model)
|
return ContentType.objects.get_for_model(self.model)
|
||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
return CustomField.objects.filter(content_types=content_type)
|
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||||
|
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
def _get_form_field(self, customfield):
|
def _get_form_field(self, customfield):
|
||||||
return customfield.to_form_field()
|
return customfield.to_form_field()
|
||||||
@ -50,13 +52,6 @@ class CustomFieldsMixin:
|
|||||||
field_name = f'cf_{customfield.name}'
|
field_name = f'cf_{customfield.name}'
|
||||||
self.fields[field_name] = self._get_form_field(customfield)
|
self.fields[field_name] = self._get_form_field(customfield)
|
||||||
|
|
||||||
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
|
||||||
self.fields[field_name].disabled = True
|
|
||||||
if self.fields[field_name].help_text:
|
|
||||||
self.fields[field_name].help_text += '<br />'
|
|
||||||
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
|
|
||||||
'Field is set to read-only.'
|
|
||||||
|
|
||||||
# Annotate the field in the list of CustomField form fields
|
# Annotate the field in the list of CustomField form fields
|
||||||
self.custom_fields[field_name] = customfield
|
self.custom_fields[field_name] = customfield
|
||||||
if customfield.group_name not in self.custom_field_groups:
|
if customfield.group_name not in self.custom_field_groups:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='journalentry',
|
model_name='journalentry',
|
||||||
name='custom_field_data',
|
name='custom_field_data',
|
||||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='journalentry',
|
model_name='journalentry',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
import decimal
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -219,14 +220,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Minimum/maximum values can be set only for numeric fields
|
# Minimum/maximum values can be set only for numeric fields
|
||||||
if self.validation_minimum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL):
|
||||||
raise ValidationError({
|
if self.validation_minimum:
|
||||||
'validation_minimum': "A minimum value may be set only for numeric fields"
|
raise ValidationError({'validation_minimum': "A minimum value may be set only for numeric fields"})
|
||||||
})
|
if self.validation_maximum:
|
||||||
if self.validation_maximum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
raise ValidationError({'validation_maximum': "A maximum value may be set only for numeric fields"})
|
||||||
raise ValidationError({
|
|
||||||
'validation_maximum': "A maximum value may be set only for numeric fields"
|
|
||||||
})
|
|
||||||
|
|
||||||
# Regex validation can be set only for text fields
|
# Regex validation can be set only for text fields
|
||||||
regex_types = (
|
regex_types = (
|
||||||
@ -297,12 +295,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
return model.objects.filter(pk__in=value)
|
return model.objects.filter(pk__in=value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
|
||||||
"""
|
"""
|
||||||
Return a form field suitable for setting a CustomField's value for an object.
|
Return a form field suitable for setting a CustomField's value for an object.
|
||||||
|
|
||||||
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
||||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||||
|
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
|
||||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||||
"""
|
"""
|
||||||
initial = self.default if set_initial else None
|
initial = self.default if set_initial else None
|
||||||
@ -317,6 +316,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
max_value=self.validation_maximum
|
max_value=self.validation_maximum
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Decimal
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
|
field = forms.DecimalField(
|
||||||
|
required=required,
|
||||||
|
initial=initial,
|
||||||
|
max_digits=12,
|
||||||
|
decimal_places=4,
|
||||||
|
min_value=self.validation_minimum,
|
||||||
|
max_value=self.validation_maximum
|
||||||
|
)
|
||||||
|
|
||||||
# Boolean
|
# Boolean
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
choices = (
|
choices = (
|
||||||
@ -398,6 +408,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
if self.description:
|
if self.description:
|
||||||
field.help_text = escape(self.description)
|
field.help_text = escape(self.description)
|
||||||
|
|
||||||
|
# Annotate read-only fields
|
||||||
|
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||||
|
field.disabled = True
|
||||||
|
prepend = '<br />' if field.help_text else ''
|
||||||
|
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
|
||||||
|
|
||||||
return field
|
return field
|
||||||
|
|
||||||
def to_filter(self, lookup_expr=None):
|
def to_filter(self, lookup_expr=None):
|
||||||
@ -426,6 +442,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
filter_class = filters.MultiValueNumberFilter
|
filter_class = filters.MultiValueNumberFilter
|
||||||
|
|
||||||
|
# Decimal
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
|
filter_class = filters.MultiValueDecimalFilter
|
||||||
|
|
||||||
# Boolean
|
# Boolean
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
filter_class = django_filters.BooleanFilter
|
filter_class = django_filters.BooleanFilter
|
||||||
@ -475,7 +495,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
||||||
|
|
||||||
# Validate integer
|
# Validate integer
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
if type(value) is not int:
|
if type(value) is not int:
|
||||||
raise ValidationError("Value must be an integer.")
|
raise ValidationError("Value must be an integer.")
|
||||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
@ -483,12 +503,23 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
||||||
|
|
||||||
|
# Validate decimal
|
||||||
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
|
try:
|
||||||
|
decimal.Decimal(value)
|
||||||
|
except decimal.InvalidOperation:
|
||||||
|
raise ValidationError("Value must be a decimal.")
|
||||||
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
|
raise ValidationError(f"Value must be at least {self.validation_minimum}")
|
||||||
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
|
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
||||||
|
|
||||||
# Validate boolean
|
# Validate boolean
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
raise ValidationError("Value must be true or false.")
|
raise ValidationError("Value must be true or false.")
|
||||||
|
|
||||||
# Validate date
|
# Validate date
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||||
if type(value) is not date:
|
if type(value) is not date:
|
||||||
try:
|
try:
|
||||||
datetime.strptime(value, '%Y-%m-%d')
|
datetime.strptime(value, '%Y-%m-%d')
|
||||||
@ -496,14 +527,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
if value not in self.choices:
|
if value not in self.choices:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate all selected choices
|
# Validate all selected choices
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
if not set(value).issubset(self.choices):
|
if not set(value).issubset(self.choices):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||||
|
@ -55,6 +55,9 @@ class PluginConfig(AppConfig):
|
|||||||
# Django-rq queues dedicated to the plugin
|
# Django-rq queues dedicated to the plugin
|
||||||
queues = []
|
queues = []
|
||||||
|
|
||||||
|
# Django apps to append to INSTALLED_APPS when plugin requires them.
|
||||||
|
django_apps = []
|
||||||
|
|
||||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||||
# integrated components.
|
# integrated components.
|
||||||
graphql_schema = 'graphql.schema'
|
graphql_schema = 'graphql.schema'
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -102,6 +104,32 @@ class CustomFieldTest(TestCase):
|
|||||||
instance.refresh_from_db()
|
instance.refresh_from_db()
|
||||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
|
def test_decimal_field(self):
|
||||||
|
|
||||||
|
# Create a custom field & check that initial value is null
|
||||||
|
cf = CustomField.objects.create(
|
||||||
|
name='decimal_field',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_DECIMAL,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
cf.content_types.set([self.object_type])
|
||||||
|
instance = Site.objects.first()
|
||||||
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
|
for value in (123456.54, 0, -123456.78):
|
||||||
|
|
||||||
|
# Assign a value and check that it is saved
|
||||||
|
instance.custom_field_data[cf.name] = value
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||||
|
|
||||||
|
# Delete the stored value and check that it is now null
|
||||||
|
instance.custom_field_data.pop(cf.name)
|
||||||
|
instance.save()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
def test_boolean_field(self):
|
def test_boolean_field(self):
|
||||||
|
|
||||||
# Create a custom field & check that initial value is null
|
# Create a custom field & check that initial value is null
|
||||||
@ -373,7 +401,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
custom_fields = (
|
custom_fields = (
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'),
|
CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'),
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC'),
|
CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC'),
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123),
|
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='integer_field', default=123),
|
||||||
|
CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45),
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False),
|
CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False),
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'),
|
CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'),
|
||||||
CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'),
|
CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'),
|
||||||
@ -424,14 +453,15 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
custom_fields[0].name: 'bar',
|
custom_fields[0].name: 'bar',
|
||||||
custom_fields[1].name: 'DEF',
|
custom_fields[1].name: 'DEF',
|
||||||
custom_fields[2].name: 456,
|
custom_fields[2].name: 456,
|
||||||
custom_fields[3].name: True,
|
custom_fields[3].name: Decimal('456.78'),
|
||||||
custom_fields[4].name: '2020-01-02',
|
custom_fields[4].name: True,
|
||||||
custom_fields[5].name: 'http://example.com/2',
|
custom_fields[5].name: '2020-01-02',
|
||||||
custom_fields[6].name: '{"foo": 1, "bar": 2}',
|
custom_fields[6].name: 'http://example.com/2',
|
||||||
custom_fields[7].name: 'Bar',
|
custom_fields[7].name: '{"foo": 1, "bar": 2}',
|
||||||
custom_fields[8].name: ['Bar', 'Baz'],
|
custom_fields[8].name: 'Bar',
|
||||||
custom_fields[9].name: vlans[1].pk,
|
custom_fields[9].name: ['Bar', 'Baz'],
|
||||||
custom_fields[10].name: [vlans[2].pk, vlans[3].pk],
|
custom_fields[10].name: vlans[1].pk,
|
||||||
|
custom_fields[11].name: [vlans[2].pk, vlans[3].pk],
|
||||||
}
|
}
|
||||||
sites[1].save()
|
sites[1].save()
|
||||||
|
|
||||||
@ -440,6 +470,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
CustomFieldTypeChoices.TYPE_TEXT: 'string',
|
CustomFieldTypeChoices.TYPE_TEXT: 'string',
|
||||||
CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
|
CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
|
||||||
CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
|
CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
|
||||||
|
CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal',
|
||||||
CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
|
CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
|
||||||
CustomFieldTypeChoices.TYPE_DATE: 'string',
|
CustomFieldTypeChoices.TYPE_DATE: 'string',
|
||||||
CustomFieldTypeChoices.TYPE_URL: 'string',
|
CustomFieldTypeChoices.TYPE_URL: 'string',
|
||||||
@ -473,7 +504,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response.data['custom_fields'], {
|
self.assertEqual(response.data['custom_fields'], {
|
||||||
'text_field': None,
|
'text_field': None,
|
||||||
'longtext_field': None,
|
'longtext_field': None,
|
||||||
'number_field': None,
|
'integer_field': None,
|
||||||
|
'decimal_field': None,
|
||||||
'boolean_field': None,
|
'boolean_field': None,
|
||||||
'date_field': None,
|
'date_field': None,
|
||||||
'url_field': None,
|
'url_field': None,
|
||||||
@ -497,7 +529,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
self.assertEqual(response.data['name'], site2.name)
|
self.assertEqual(response.data['name'], site2.name)
|
||||||
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
|
self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
|
self.assertEqual(response.data['custom_fields']['integer_field'], site2_cfvs['integer_field'])
|
||||||
|
self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
|
self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
||||||
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
||||||
@ -531,7 +564,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
response_cf = response.data['custom_fields']
|
response_cf = response.data['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
|
||||||
|
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||||
@ -548,7 +582,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
site = Site.objects.get(pk=response.data['id'])
|
site = Site.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||||
@ -568,7 +603,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
'custom_fields': {
|
'custom_fields': {
|
||||||
'text_field': 'bar',
|
'text_field': 'bar',
|
||||||
'longtext_field': 'blah blah blah',
|
'longtext_field': 'blah blah blah',
|
||||||
'number_field': 456,
|
'integer_field': 456,
|
||||||
|
'decimal_field': 456.78,
|
||||||
'boolean_field': True,
|
'boolean_field': True,
|
||||||
'date_field': '2020-01-02',
|
'date_field': '2020-01-02',
|
||||||
'url_field': 'http://example.com/2',
|
'url_field': 'http://example.com/2',
|
||||||
@ -590,7 +626,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
data_cf = data['custom_fields']
|
data_cf = data['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], data_cf['text_field'])
|
self.assertEqual(response_cf['text_field'], data_cf['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
|
self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], data_cf['number_field'])
|
self.assertEqual(response_cf['integer_field'], data_cf['integer_field'])
|
||||||
|
self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
|
self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
||||||
@ -607,7 +644,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
site = Site.objects.get(pk=response.data['id'])
|
site = Site.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
|
self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
|
self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], data_cf['number_field'])
|
self.assertEqual(site.custom_field_data['integer_field'], data_cf['integer_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
|
self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
|
||||||
@ -652,7 +690,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
response_cf = response.data[i]['custom_fields']
|
response_cf = response.data[i]['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
|
||||||
|
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||||
@ -669,7 +708,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
site = Site.objects.get(pk=response.data[i]['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||||
@ -686,7 +726,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
custom_field_data = {
|
custom_field_data = {
|
||||||
'text_field': 'bar',
|
'text_field': 'bar',
|
||||||
'longtext_field': 'abcdefghij',
|
'longtext_field': 'abcdefghij',
|
||||||
'number_field': 456,
|
'integer_field': 456,
|
||||||
|
'decimal_field': 456.78,
|
||||||
'boolean_field': True,
|
'boolean_field': True,
|
||||||
'date_field': '2020-01-02',
|
'date_field': '2020-01-02',
|
||||||
'url_field': 'http://example.com/2',
|
'url_field': 'http://example.com/2',
|
||||||
@ -726,7 +767,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
response_cf = response.data[i]['custom_fields']
|
response_cf = response.data[i]['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
|
self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
|
||||||
self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
|
self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
|
||||||
self.assertEqual(response_cf['number_field'], custom_field_data['number_field'])
|
self.assertEqual(response_cf['integer_field'], custom_field_data['integer_field'])
|
||||||
|
self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
|
self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
||||||
@ -743,7 +785,8 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
site = Site.objects.get(pk=response.data[i]['id'])
|
site = Site.objects.get(pk=response.data[i]['id'])
|
||||||
self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
|
self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
|
||||||
self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
|
self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
|
||||||
self.assertEqual(site.custom_field_data['number_field'], custom_field_data['number_field'])
|
self.assertEqual(site.custom_field_data['integer_field'], custom_field_data['integer_field'])
|
||||||
|
self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field'])
|
||||||
self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
|
self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
|
||||||
self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
|
self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
|
||||||
self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
|
self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
|
||||||
@ -763,7 +806,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
data = {
|
data = {
|
||||||
'custom_fields': {
|
'custom_fields': {
|
||||||
'text_field': 'ABCD',
|
'text_field': 'ABCD',
|
||||||
'number_field': 1234,
|
'integer_field': 1234,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
@ -775,8 +818,9 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
# Validate response data
|
# Validate response data
|
||||||
response_cf = response.data['custom_fields']
|
response_cf = response.data['custom_fields']
|
||||||
self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
|
self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
|
||||||
self.assertEqual(response_cf['number_field'], data['custom_fields']['number_field'])
|
|
||||||
self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
|
self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
|
||||||
|
self.assertEqual(response_cf['integer_field'], data['custom_fields']['integer_field'])
|
||||||
|
self.assertEqual(response_cf['decimal_field'], original_cfvs['decimal_field'])
|
||||||
self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
|
self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
|
||||||
self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
|
self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
|
||||||
self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
|
self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
|
||||||
@ -792,8 +836,9 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
# Validate database data
|
# Validate database data
|
||||||
site2.refresh_from_db()
|
site2.refresh_from_db()
|
||||||
self.assertEqual(site2.custom_field_data['text_field'], data['custom_fields']['text_field'])
|
self.assertEqual(site2.custom_field_data['text_field'], data['custom_fields']['text_field'])
|
||||||
self.assertEqual(site2.custom_field_data['number_field'], data['custom_fields']['number_field'])
|
|
||||||
self.assertEqual(site2.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
|
self.assertEqual(site2.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
|
||||||
|
self.assertEqual(site2.custom_field_data['integer_field'], data['custom_fields']['integer_field'])
|
||||||
|
self.assertEqual(site2.custom_field_data['decimal_field'], original_cfvs['decimal_field'])
|
||||||
self.assertEqual(site2.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
|
self.assertEqual(site2.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
|
||||||
self.assertEqual(site2.custom_field_data['date_field'], original_cfvs['date_field'])
|
self.assertEqual(site2.custom_field_data['date_field'], original_cfvs['date_field'])
|
||||||
self.assertEqual(site2.custom_field_data['url_field'], original_cfvs['url_field'])
|
self.assertEqual(site2.custom_field_data['url_field'], original_cfvs['url_field'])
|
||||||
@ -808,20 +853,20 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||||
self.add_permissions('dcim.change_site')
|
self.add_permissions('dcim.change_site')
|
||||||
|
|
||||||
cf_integer = CustomField.objects.get(name='number_field')
|
cf_integer = CustomField.objects.get(name='integer_field')
|
||||||
cf_integer.validation_minimum = 10
|
cf_integer.validation_minimum = 10
|
||||||
cf_integer.validation_maximum = 20
|
cf_integer.validation_maximum = 20
|
||||||
cf_integer.save()
|
cf_integer.save()
|
||||||
|
|
||||||
data = {'custom_fields': {'number_field': 9}}
|
data = {'custom_fields': {'integer_field': 9}}
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data = {'custom_fields': {'number_field': 21}}
|
data = {'custom_fields': {'integer_field': 21}}
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data = {'custom_fields': {'number_field': 15}}
|
data = {'custom_fields': {'integer_field': 15}}
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -860,6 +905,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
|
CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||||
CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
|
CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
|
||||||
CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
|
CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
|
||||||
|
CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL),
|
||||||
CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
|
CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
|
||||||
CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
|
CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
|
||||||
CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
|
CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
|
||||||
@ -880,10 +926,10 @@ class CustomFieldImportTest(TestCase):
|
|||||||
Import a Site in CSV format, including a value for each CustomField.
|
Import a Site in CSV format, including a value for each CustomField.
|
||||||
"""
|
"""
|
||||||
data = (
|
data = (
|
||||||
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
||||||
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
|
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
|
||||||
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
|
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
|
||||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''),
|
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', ''),
|
||||||
)
|
)
|
||||||
csv_data = '\n'.join(','.join(row) for row in data)
|
csv_data = '\n'.join(','.join(row) for row in data)
|
||||||
|
|
||||||
@ -893,10 +939,11 @@ class CustomFieldImportTest(TestCase):
|
|||||||
|
|
||||||
# Validate data for site 1
|
# Validate data for site 1
|
||||||
site1 = Site.objects.get(name='Site 1')
|
site1 = Site.objects.get(name='Site 1')
|
||||||
self.assertEqual(len(site1.custom_field_data), 9)
|
self.assertEqual(len(site1.custom_field_data), 10)
|
||||||
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
||||||
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
||||||
self.assertEqual(site1.custom_field_data['integer'], 123)
|
self.assertEqual(site1.custom_field_data['integer'], 123)
|
||||||
|
self.assertEqual(site1.custom_field_data['decimal'], 123.45)
|
||||||
self.assertEqual(site1.custom_field_data['boolean'], True)
|
self.assertEqual(site1.custom_field_data['boolean'], True)
|
||||||
self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
|
self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
|
||||||
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
||||||
@ -906,10 +953,11 @@ class CustomFieldImportTest(TestCase):
|
|||||||
|
|
||||||
# Validate data for site 2
|
# Validate data for site 2
|
||||||
site2 = Site.objects.get(name='Site 2')
|
site2 = Site.objects.get(name='Site 2')
|
||||||
self.assertEqual(len(site2.custom_field_data), 9)
|
self.assertEqual(len(site2.custom_field_data), 10)
|
||||||
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
||||||
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
||||||
self.assertEqual(site2.custom_field_data['integer'], 456)
|
self.assertEqual(site2.custom_field_data['integer'], 456)
|
||||||
|
self.assertEqual(site2.custom_field_data['decimal'], 456.78)
|
||||||
self.assertEqual(site2.custom_field_data['boolean'], False)
|
self.assertEqual(site2.custom_field_data['boolean'], False)
|
||||||
self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
|
self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
|
||||||
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
||||||
@ -1034,53 +1082,78 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
|
# Decimal filtering
|
||||||
|
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||||
|
cf.save()
|
||||||
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Boolean filtering
|
# Boolean filtering
|
||||||
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Exact text filtering
|
# Exact text filtering
|
||||||
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT,
|
cf = CustomField(
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
name='cf4',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Loose text filtering
|
# Loose text filtering
|
||||||
cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
|
cf = CustomField(
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
name='cf5',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Date filtering
|
# Date filtering
|
||||||
cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE)
|
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Exact URL filtering
|
# Exact URL filtering
|
||||||
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL,
|
cf = CustomField(
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
name='cf7',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_URL,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Loose URL filtering
|
# Loose URL filtering
|
||||||
cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
|
cf = CustomField(
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
name='cf8',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_URL,
|
||||||
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Selection filtering
|
# Selection filtering
|
||||||
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
|
cf = CustomField(
|
||||||
|
name='cf9',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
|
choices=['Foo', 'Bar', 'Baz']
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Multiselect filtering
|
# Multiselect filtering
|
||||||
cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'B', 'C', 'X'])
|
cf = CustomField(
|
||||||
|
name='cf10',
|
||||||
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||||
|
choices=['A', 'B', 'C', 'X']
|
||||||
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.content_types.set([obj_type])
|
||||||
|
|
||||||
# Object filtering
|
# Object filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
name='cf10',
|
name='cf11',
|
||||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||||
)
|
)
|
||||||
@ -1089,7 +1162,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
|
|
||||||
# Multi-object filtering
|
# Multi-object filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
name='cf11',
|
name='cf12',
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||||
)
|
)
|
||||||
@ -1099,42 +1172,45 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
Site.objects.bulk_create([
|
Site.objects.bulk_create([
|
||||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||||
'cf1': 100,
|
'cf1': 100,
|
||||||
'cf2': True,
|
'cf2': 100.1,
|
||||||
'cf3': 'foo',
|
'cf3': True,
|
||||||
'cf4': 'foo',
|
'cf4': 'foo',
|
||||||
'cf5': '2016-06-26',
|
'cf5': 'foo',
|
||||||
'cf6': 'http://a.example.com',
|
'cf6': '2016-06-26',
|
||||||
'cf7': 'http://a.example.com',
|
'cf7': 'http://a.example.com',
|
||||||
'cf8': 'Foo',
|
'cf8': 'http://a.example.com',
|
||||||
'cf9': ['A', 'X'],
|
'cf9': 'Foo',
|
||||||
'cf10': manufacturers[0].pk,
|
'cf10': ['A', 'X'],
|
||||||
'cf11': [manufacturers[0].pk, manufacturers[3].pk],
|
'cf11': manufacturers[0].pk,
|
||||||
|
'cf12': [manufacturers[0].pk, manufacturers[3].pk],
|
||||||
}),
|
}),
|
||||||
Site(name='Site 2', slug='site-2', custom_field_data={
|
Site(name='Site 2', slug='site-2', custom_field_data={
|
||||||
'cf1': 200,
|
'cf1': 200,
|
||||||
'cf2': True,
|
'cf2': 200.2,
|
||||||
'cf3': 'foobar',
|
'cf3': True,
|
||||||
'cf4': 'foobar',
|
'cf4': 'foobar',
|
||||||
'cf5': '2016-06-27',
|
'cf5': 'foobar',
|
||||||
'cf6': 'http://b.example.com',
|
'cf6': '2016-06-27',
|
||||||
'cf7': 'http://b.example.com',
|
'cf7': 'http://b.example.com',
|
||||||
'cf8': 'Bar',
|
'cf8': 'http://b.example.com',
|
||||||
'cf9': ['B', 'X'],
|
'cf9': 'Bar',
|
||||||
'cf10': manufacturers[1].pk,
|
'cf10': ['B', 'X'],
|
||||||
'cf11': [manufacturers[1].pk, manufacturers[3].pk],
|
'cf11': manufacturers[1].pk,
|
||||||
|
'cf12': [manufacturers[1].pk, manufacturers[3].pk],
|
||||||
}),
|
}),
|
||||||
Site(name='Site 3', slug='site-3', custom_field_data={
|
Site(name='Site 3', slug='site-3', custom_field_data={
|
||||||
'cf1': 300,
|
'cf1': 300,
|
||||||
'cf2': False,
|
'cf2': 300.3,
|
||||||
'cf3': 'bar',
|
'cf3': False,
|
||||||
'cf4': 'bar',
|
'cf4': 'bar',
|
||||||
'cf5': '2016-06-28',
|
'cf5': 'bar',
|
||||||
'cf6': 'http://c.example.com',
|
'cf6': '2016-06-28',
|
||||||
'cf7': 'http://c.example.com',
|
'cf7': 'http://c.example.com',
|
||||||
'cf8': 'Baz',
|
'cf8': 'http://c.example.com',
|
||||||
'cf9': ['C', 'X'],
|
'cf9': 'Baz',
|
||||||
'cf10': manufacturers[2].pk,
|
'cf10': ['C', 'X'],
|
||||||
'cf11': [manufacturers[2].pk, manufacturers[3].pk],
|
'cf11': manufacturers[2].pk,
|
||||||
|
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -1146,60 +1222,68 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_filter_decimal(self):
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2)
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2__n': [200.2]}, self.queryset).qs.count(), 2)
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2__gt': [200.2]}, self.queryset).qs.count(), 1)
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2)
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1)
|
||||||
|
self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_boolean(self):
|
def test_filter_boolean(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf3': False}, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_filter_text_strict(self):
|
def test_filter_text_strict(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf3': ['foo']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__n': ['foo']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf4__n': ['foo']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__ic': ['foo']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf4__ic': ['foo']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__nic': ['foo']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf4__nic': ['foo']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__isw': ['foo']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf4__isw': ['foo']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__nisw': ['foo']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf4__nisw': ['foo']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__iew': ['bar']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf4__iew': ['bar']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__niew': ['bar']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__ie': ['FOO']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf3__nie': ['FOO']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_text_loose(self):
|
def test_filter_text_loose(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_date(self):
|
def test_filter_date(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf5': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf6': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf5__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf6__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf5__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf6__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf5__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf5__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf5__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_url_strict(self):
|
def test_filter_url_strict(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf6': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf7': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf7__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__ic': ['b']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf7__ic': ['b']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__nic': ['b']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf7__nic': ['b']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__isw': ['http://']}, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset({'cf_cf7__isw': ['http://']}, self.queryset).qs.count(), 3)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__nisw': ['http://']}, self.queryset).qs.count(), 0)
|
self.assertEqual(self.filterset({'cf_cf7__nisw': ['http://']}, self.queryset).qs.count(), 0)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__iew': ['.com']}, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset({'cf_cf7__iew': ['.com']}, self.queryset).qs.count(), 3)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__niew': ['.com']}, self.queryset).qs.count(), 0)
|
self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset({'cf_cf6__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_url_loose(self):
|
def test_filter_url_loose(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf7': ['example.com']}, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_filter_select(self):
|
def test_filter_select(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf8': ['Foo', 'Bar']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf9': ['Foo', 'Bar']}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_multiselect(self):
|
def test_filter_multiselect(self):
|
||||||
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf10': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset({'cf_cf10': ['X']}, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_filter_object(self):
|
def test_filter_object(self):
|
||||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||||
self.assertEqual(self.filterset({'cf_cf10': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_filter_multiobject(self):
|
def test_filter_multiobject(self):
|
||||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset({'cf_cf12': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)
|
||||||
|
@ -23,6 +23,9 @@ class CustomFieldModelFormTest(TestCase):
|
|||||||
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
||||||
cf_integer.content_types.set([obj_type])
|
cf_integer.content_types.set([obj_type])
|
||||||
|
|
||||||
|
cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||||
|
cf_integer.content_types.set([obj_type])
|
||||||
|
|
||||||
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||||
cf_boolean.content_types.set([obj_type])
|
cf_boolean.content_types.set([obj_type])
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('prefix', ipam.fields.IPNetworkField()),
|
('prefix', ipam.fields.IPNetworkField()),
|
||||||
('date_added', models.DateField(blank=True, null=True)),
|
('date_added', models.DateField(blank=True, null=True)),
|
||||||
@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('address', ipam.fields.IPAddressField()),
|
('address', ipam.fields.IPAddressField()),
|
||||||
('status', models.CharField(default='active', max_length=50)),
|
('status', models.CharField(default='active', max_length=50)),
|
||||||
@ -64,7 +64,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('prefix', ipam.fields.IPNetworkField()),
|
('prefix', ipam.fields.IPNetworkField()),
|
||||||
('status', models.CharField(default='active', max_length=50)),
|
('status', models.CharField(default='active', max_length=50)),
|
||||||
@ -81,7 +81,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -99,7 +99,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -115,7 +115,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=21, unique=True)),
|
('name', models.CharField(max_length=21, unique=True)),
|
||||||
('description', models.CharField(blank=True, max_length=200)),
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
@ -129,7 +129,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('rd', models.CharField(blank=True, max_length=21, null=True, unique=True)),
|
('rd', models.CharField(blank=True, max_length=21, null=True, unique=True)),
|
||||||
@ -151,7 +151,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('slug', models.SlugField(max_length=100)),
|
('slug', models.SlugField(max_length=100)),
|
||||||
@ -170,7 +170,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
('vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
@ -193,7 +193,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('protocol', models.CharField(max_length=50)),
|
('protocol', models.CharField(max_length=50)),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Generated by Django 3.2.5 on 2021-07-16 14:15
|
# Generated by Django 3.2.5 on 2021-07-16 14:15
|
||||||
|
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.db.models.expressions
|
import django.db.models.expressions
|
||||||
@ -22,7 +22,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('start_address', ipam.fields.IPAddressField()),
|
('start_address', ipam.fields.IPAddressField()),
|
||||||
('end_address', ipam.fields.IPAddressField()),
|
('end_address', ipam.fields.IPAddressField()),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -19,7 +19,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('group_id', models.PositiveSmallIntegerField()),
|
('group_id', models.PositiveSmallIntegerField()),
|
||||||
('protocol', models.CharField(max_length=50)),
|
('protocol', models.CharField(max_length=50)),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 3.2.8 on 2021-11-02 16:16
|
# Generated by Django 3.2.8 on 2021-11-02 16:16
|
||||||
|
|
||||||
import dcim.fields
|
import dcim.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('asn', dcim.fields.ASNField(unique=True)),
|
('asn', dcim.fields.ASNField(unique=True)),
|
||||||
('description', models.CharField(blank=True, max_length=200)),
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('protocol', models.CharField(max_length=50)),
|
('protocol', models.CharField(max_length=50)),
|
||||||
('ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]), size=None)),
|
('ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]), size=None)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField()),
|
('slug', models.SlugField()),
|
||||||
('type', models.CharField(max_length=50)),
|
('type', models.CharField(max_length=50)),
|
||||||
@ -42,7 +42,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('assigned_object_id', models.PositiveBigIntegerField()),
|
('assigned_object_id', models.PositiveBigIntegerField()),
|
||||||
('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||||
('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='ipam.l2vpn')),
|
('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='ipam.l2vpn')),
|
||||||
|
@ -46,7 +46,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
'filter_class': filters.MultiValueDateTimeFilter
|
'filter_class': filters.MultiValueDateTimeFilter
|
||||||
},
|
},
|
||||||
models.DecimalField: {
|
models.DecimalField: {
|
||||||
'filter_class': filters.MultiValueNumberFilter
|
'filter_class': filters.MultiValueDecimalFilter
|
||||||
},
|
},
|
||||||
models.EmailField: {
|
models.EmailField: {
|
||||||
'filter_class': filters.MultiValueCharFilter
|
'filter_class': filters.MultiValueCharFilter
|
||||||
@ -95,6 +95,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
filters.MultiValueDateFilter,
|
filters.MultiValueDateFilter,
|
||||||
filters.MultiValueDateTimeFilter,
|
filters.MultiValueDateTimeFilter,
|
||||||
filters.MultiValueNumberFilter,
|
filters.MultiValueNumberFilter,
|
||||||
|
filters.MultiValueDecimalFilter,
|
||||||
filters.MultiValueTimeFilter
|
filters.MultiValueTimeFilter
|
||||||
)):
|
)):
|
||||||
return FILTER_NUMERIC_BASED_LOOKUP_MAP
|
return FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
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 extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
|
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
|
||||||
from extras.forms.customfields import CustomFieldsMixin
|
from extras.forms.customfields import CustomFieldsMixin
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, Tag
|
||||||
from utilities.forms import BootstrapMixin, CSVModelForm
|
from utilities.forms import BootstrapMixin, CSVModelForm
|
||||||
@ -63,6 +63,11 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
|
|||||||
"""
|
"""
|
||||||
tags = None # Temporary fix in lieu of tag import support (see #9158)
|
tags = None # Temporary fix in lieu of tag import support (see #9158)
|
||||||
|
|
||||||
|
def _get_custom_fields(self, content_type):
|
||||||
|
return CustomField.objects.filter(content_types=content_type).filter(
|
||||||
|
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
|
||||||
|
)
|
||||||
|
|
||||||
def _get_form_field(self, customfield):
|
def _get_form_field(self, customfield):
|
||||||
return customfield.to_form_field(for_csv_import=True)
|
return customfield.to_form_field(for_csv_import=True)
|
||||||
|
|
||||||
@ -125,10 +130,10 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
return CustomField.objects.filter(content_types=content_type).exclude(
|
return super()._get_custom_fields(content_type).exclude(
|
||||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_form_field(self, customfield):
|
def _get_form_field(self, customfield):
|
||||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)
|
||||||
|
@ -4,7 +4,6 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||||||
from django.db.models.signals import class_prepared
|
from django.db.models.signals import class_prepared
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
@ -12,6 +11,7 @@ from taggit.managers import TaggableManager
|
|||||||
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||||
from extras.utils import is_taggable, register_features
|
from extras.utils import is_taggable, register_features
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -124,7 +124,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
Enables support for custom fields.
|
Enables support for custom fields.
|
||||||
"""
|
"""
|
||||||
custom_field_data = models.JSONField(
|
custom_field_data = models.JSONField(
|
||||||
encoder=DjangoJSONEncoder,
|
encoder=CustomFieldJSONEncoder,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=dict
|
default=dict
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
import django
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
|
from django.utils.encoding import force_str
|
||||||
|
from extras.plugins import PluginConfig
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
|
|
||||||
from netbox.config import PARAMS
|
from netbox.config import PARAMS
|
||||||
@ -20,9 +21,7 @@ from netbox.config import PARAMS
|
|||||||
# Monkey patch to fix Django 4.0 support for graphene-django (see
|
# Monkey patch to fix Django 4.0 support for graphene-django (see
|
||||||
# https://github.com/graphql-python/graphene-django/issues/1284)
|
# https://github.com/graphql-python/graphene-django/issues/1284)
|
||||||
# TODO: Remove this when graphene-django 2.16 becomes available
|
# TODO: Remove this when graphene-django 2.16 becomes available
|
||||||
import django
|
django.utils.encoding.force_text = force_str # type: ignore
|
||||||
from django.utils.encoding import force_str
|
|
||||||
django.utils.encoding.force_text = force_str
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -186,7 +185,7 @@ if STORAGE_BACKEND is not None:
|
|||||||
if STORAGE_BACKEND.startswith('storages.'):
|
if STORAGE_BACKEND.startswith('storages.'):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import storages.utils
|
import storages.utils # type: ignore
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
if getattr(e, 'name') == 'storages':
|
if getattr(e, 'name') == 'storages':
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
@ -663,14 +662,42 @@ for plugin_name in PLUGINS:
|
|||||||
|
|
||||||
# Determine plugin config and add to INSTALLED_APPS.
|
# Determine plugin config and add to INSTALLED_APPS.
|
||||||
try:
|
try:
|
||||||
plugin_config = plugin.config
|
plugin_config: PluginConfig = plugin.config
|
||||||
INSTALLED_APPS.append("{}.{}".format(plugin_config.__module__, plugin_config.__name__))
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Plugin {} does not provide a 'config' variable. This should be defined in the plugin's __init__.py file "
|
"Plugin {} does not provide a 'config' variable. This should be defined in the plugin's __init__.py file "
|
||||||
"and point to the PluginConfig subclass.".format(plugin_name)
|
"and point to the PluginConfig subclass.".format(plugin_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
plugin_module = "{}.{}".format(plugin_config.__module__, plugin_config.__name__) # type: ignore
|
||||||
|
|
||||||
|
# Gather additional apps to load alongside this plugin
|
||||||
|
django_apps = plugin_config.django_apps
|
||||||
|
if plugin_name in django_apps:
|
||||||
|
django_apps.pop(plugin_name)
|
||||||
|
if plugin_module not in django_apps:
|
||||||
|
django_apps.append(plugin_module)
|
||||||
|
|
||||||
|
# Test if we can import all modules (or its parent, for PluginConfigs and AppConfigs)
|
||||||
|
for app in django_apps:
|
||||||
|
if "." in app:
|
||||||
|
parts = app.split(".")
|
||||||
|
spec = importlib.util.find_spec(".".join(parts[:-1]))
|
||||||
|
else:
|
||||||
|
spec = importlib.util.find_spec(app)
|
||||||
|
if spec is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"Failed to load django_apps specified by plugin {plugin_name}: {django_apps} "
|
||||||
|
f"The module {app} cannot be imported. Check that the necessary package has been "
|
||||||
|
"installed within the correct Python environment."
|
||||||
|
)
|
||||||
|
|
||||||
|
INSTALLED_APPS.extend(django_apps)
|
||||||
|
|
||||||
|
# Preserve uniqueness of the INSTALLED_APPS list, we keep the last occurence
|
||||||
|
sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
|
||||||
|
INSTALLED_APPS = list(sorted_apps)
|
||||||
|
|
||||||
# Validate user-provided configuration settings and assign defaults
|
# Validate user-provided configuration settings and assign defaults
|
||||||
if plugin_name not in PLUGINS_CONFIG:
|
if plugin_name not in PLUGINS_CONFIG:
|
||||||
PLUGINS_CONFIG[plugin_name] = {}
|
PLUGINS_CONFIG[plugin_name] = {}
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
{% with object.parent_bay.device as parent %}
|
{% with object.parent_bay.device as parent %}
|
||||||
{{ parent|linkify }} / {{ object.parent_bay }}
|
{{ parent|linkify }} / {{ object.parent_bay }}
|
||||||
{% if parent.position %}
|
{% if parent.position %}
|
||||||
(U{{ parent.position }} / {{ parent.get_face_display }})
|
(U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif object.rack and object.position %}
|
{% elif object.rack and object.position %}
|
||||||
@ -90,7 +90,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Device Type</th>
|
<th scope="row">Device Type</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height }}U)
|
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -29,12 +29,22 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Height (U)</td>
|
<td>Height (U)</td>
|
||||||
<td>{{ object.u_height }}</td>
|
<td>{{ object.u_height|floatformat }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Full Depth</td>
|
<td>Full Depth</td>
|
||||||
<td>{% checkmark object.is_full_depth %}</td>
|
<td>{% checkmark object.is_full_depth %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Weight</td>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent/Child</td>
|
<td>Parent/Child</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -22,6 +22,16 @@
|
|||||||
<td>Part Number</td>
|
<td>Part Number</td>
|
||||||
<td>{{ object.part_number|placeholder }}</td>
|
<td>{{ object.part_number|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Weight</td>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
||||||
|
@ -104,9 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Dimensions</h5>
|
||||||
Dimensions
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -147,6 +145,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Rack Weight</th>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Total Weight</th>
|
||||||
|
<td>{{ object.total_weight|floatformat }} Kilograms</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,6 +198,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
|
@ -57,6 +57,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_field form.desc_units %}
|
{% render_field form.desc_units %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Weight</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.weight %}
|
||||||
|
{% render_field form.weight_unit %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">Contact Assignment</h5>
|
<h5 class="offset-sm-3">Contact Assignment</h5>
|
||||||
|
@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fields = (
|
fields = (
|
||||||
'group', 'contact', 'role', 'priority',
|
'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'content_type': forms.HiddenInput(),
|
||||||
|
'object_id': forms.HiddenInput(),
|
||||||
'priority': StaticSelect(),
|
'priority': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import mptt.fields
|
import mptt.fields
|
||||||
@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import mptt.fields
|
import mptt.fields
|
||||||
@ -19,7 +19,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('slug', models.SlugField(max_length=100)),
|
('slug', models.SlugField(max_length=100)),
|
||||||
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('title', models.CharField(blank=True, max_length=100)),
|
('title', models.CharField(blank=True, max_length=100)),
|
||||||
|
@ -3,8 +3,6 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django_filters.constants import EMPTY_VALUES
|
from django_filters.constants import EMPTY_VALUES
|
||||||
|
|
||||||
from utilities.forms import MACAddressField
|
|
||||||
|
|
||||||
|
|
||||||
def multivalue_field_factory(field_class):
|
def multivalue_field_factory(field_class):
|
||||||
"""
|
"""
|
||||||
@ -23,7 +21,15 @@ def multivalue_field_factory(field_class):
|
|||||||
field.to_python(v) for v in value if v
|
field.to_python(v) for v in value if v
|
||||||
]
|
]
|
||||||
|
|
||||||
return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
|
def run_validators(self, value):
|
||||||
|
for v in value:
|
||||||
|
super().run_validators(v)
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
for v in value:
|
||||||
|
super().validate(v)
|
||||||
|
|
||||||
|
return type(f'MultiValue{field_class.__name__}', (NewField,), dict())
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -46,6 +52,10 @@ class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
|||||||
field_class = multivalue_field_factory(forms.IntegerField)
|
field_class = multivalue_field_factory(forms.IntegerField)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.DecimalField)
|
||||||
|
|
||||||
|
|
||||||
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.TimeField)
|
field_class = multivalue_field_factory(forms.TimeField)
|
||||||
|
|
||||||
|
17
netbox/utilities/json.py
Normal file
17
netbox/utilities/json.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'CustomFieldJSONEncoder',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldJSONEncoder(DjangoJSONEncoder):
|
||||||
|
"""
|
||||||
|
Override Django's built-in JSON encoder to save decimal values as JSON numbers.
|
||||||
|
"""
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, decimal.Decimal):
|
||||||
|
return float(o)
|
||||||
|
return super().default(o)
|
@ -73,9 +73,9 @@ def humanize_megabytes(mb):
|
|||||||
"""
|
"""
|
||||||
if not mb:
|
if not mb:
|
||||||
return ''
|
return ''
|
||||||
if mb >= 1048576:
|
if not mb % 1048576: # 1024^2
|
||||||
return f'{int(mb / 1048576)} TB'
|
return f'{int(mb / 1048576)} TB'
|
||||||
if mb >= 1024:
|
if not mb % 1024:
|
||||||
return f'{int(mb / 1024)} GB'
|
return f'{int(mb / 1024)} GB'
|
||||||
return f'{mb} MB'
|
return f'{mb} MB'
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from django.http import QueryDict
|
|||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
from mptt.models import MPTTModel
|
from mptt.models import MPTTModel
|
||||||
|
|
||||||
from dcim.choices import CableLengthUnitChoices
|
from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
|
||||||
from extras.plugins import PluginConfig
|
from extras.plugins import PluginConfig
|
||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
@ -270,6 +270,31 @@ def to_meters(length, unit):
|
|||||||
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
||||||
|
|
||||||
|
|
||||||
|
def to_grams(weight, unit):
|
||||||
|
"""
|
||||||
|
Convert the given weight to kilograms.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if weight < 0:
|
||||||
|
raise ValueError("Weight must be a positive number")
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError(f"Invalid value '{weight}' for weight (must be a number)")
|
||||||
|
|
||||||
|
valid_units = WeightUnitChoices.values()
|
||||||
|
if unit not in valid_units:
|
||||||
|
raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}")
|
||||||
|
|
||||||
|
if unit == WeightUnitChoices.UNIT_KILOGRAM:
|
||||||
|
return weight * 1000
|
||||||
|
if unit == WeightUnitChoices.UNIT_GRAM:
|
||||||
|
return weight
|
||||||
|
if unit == WeightUnitChoices.UNIT_POUND:
|
||||||
|
return weight * Decimal(453.592)
|
||||||
|
if unit == WeightUnitChoices.UNIT_OUNCE:
|
||||||
|
return weight * Decimal(28.3495)
|
||||||
|
raise ValueError(f"Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.")
|
||||||
|
|
||||||
|
|
||||||
def render_jinja2(template_code, context):
|
def render_jinja2(template_code, context):
|
||||||
"""
|
"""
|
||||||
Render a Jinja2 template with the provided context. Return the rendered content.
|
Render a Jinja2 template with the provided context. Return the rendered content.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import dcim.fields
|
import dcim.fields
|
||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -51,7 +51,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('comments', models.TextField(blank=True)),
|
('comments', models.TextField(blank=True)),
|
||||||
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -80,7 +80,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -95,7 +95,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('local_context_data', models.JSONField(blank=True, null=True)),
|
('local_context_data', models.JSONField(blank=True, null=True)),
|
||||||
('name', models.CharField(max_length=64)),
|
('name', models.CharField(max_length=64)),
|
||||||
@ -147,7 +147,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('enabled', models.BooleanField(default=True)),
|
('enabled', models.BooleanField(default=True)),
|
||||||
('mac_address', dcim.fields.MACAddressField(blank=True, null=True)),
|
('mac_address', dcim.fields.MACAddressField(blank=True, null=True)),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import django.core.serializers.json
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import mptt.fields
|
import mptt.fields
|
||||||
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, unique=True)),
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
('slug', models.SlugField(max_length=100, unique=True)),
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('ssid', models.CharField(max_length=32)),
|
('ssid', models.CharField(max_length=32)),
|
||||||
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')),
|
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')),
|
||||||
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('created', models.DateField(auto_now_add=True, null=True)),
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder)),
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
('ssid', models.CharField(blank=True, max_length=32)),
|
('ssid', models.CharField(blank=True, max_length=32)),
|
||||||
('status', models.CharField(default='connected', max_length=50)),
|
('status', models.CharField(default='connected', max_length=50)),
|
||||||
|
Loading…
Reference in New Issue
Block a user