mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #6659 from netbox-community/6466-replace-django-admin
Closes #6646: Move extras out of Django admin UI
This commit is contained in:
commit
442b3fcc48
@ -1,6 +1,6 @@
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
|
|
||||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks.
|
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are managed under Logging > Webhooks.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ CORS_ORIGIN_WHITELIST = [
|
|||||||
|
|
||||||
## CUSTOM_VALIDATORS
|
## CUSTOM_VALIDATORS
|
||||||
|
|
||||||
This is a mapping of models to [custom validators](../additional-features/custom-validation.md) that have been defined locally to enforce custom validation logic.
|
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -501,7 +501,7 @@ This parameter defines the URL of the repository that will be checked periodical
|
|||||||
|
|
||||||
Default: `$INSTALL_ROOT/netbox/reports/`
|
Default: `$INSTALL_ROOT/netbox/reports/`
|
||||||
|
|
||||||
The file path to the location where custom reports will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
|
The file path to the location where [custom reports](../customization/reports.md) will be kept. By default, this is the `netbox/reports/` directory within the base NetBox installation path.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -517,7 +517,7 @@ The maximum execution time of a background task (such as running a custom script
|
|||||||
|
|
||||||
Default: `$INSTALL_ROOT/netbox/scripts/`
|
Default: `$INSTALL_ROOT/netbox/scripts/`
|
||||||
|
|
||||||
The file path to the location where custom scripts will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path.
|
The file path to the location where [custom scripts](../customization/custom-scripts.md) will be kept. By default, this is the `netbox/scripts/` directory within the base NetBox installation path.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
|
|||||||
|
|
||||||
## Creating Custom Fields
|
## Creating Custom Fields
|
||||||
|
|
||||||
Custom fields must be created through the admin UI under Extras > Custom Fields. NetBox supports six types of custom field:
|
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
|
||||||
|
|
||||||
* Text: Free-form text (up to 255 characters)
|
* Text: Free-form text (up to 255 characters)
|
||||||
* Integer: A whole number (positive or negative)
|
* Integer: A whole number (positive or negative)
|
@ -1,8 +1,8 @@
|
|||||||
# Custom Links
|
# Custom Links
|
||||||
|
|
||||||
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside of NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
|
||||||
|
|
||||||
Custom links are created under the admin UI. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
|
||||||
|
|
||||||
For example, you might define a link like this:
|
For example, you might define a link like this:
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as:
|
|||||||
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
|
<a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
|
Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
|
||||||
|
|
||||||
## Context Data
|
## Context Data
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
# Export Templates
|
# Export Templates
|
||||||
|
|
||||||
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
|
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Customization > Export Templates.
|
||||||
|
|
||||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
|
||||||
|
|
@ -10,8 +10,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
|||||||
|
|
||||||
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
|
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
|
||||||
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
|
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
|
||||||
* [Custom fields](../additional-features/custom-fields.md) - These models support the addition of user-defined fields
|
* [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields
|
||||||
* [Export templates](../additional-features/export-templates.md) - Users can create custom export templates for these models
|
* [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models
|
||||||
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
|
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
|
||||||
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
|
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
|
||||||
* Nesting - These models can be nested recursively to create a hierarchy
|
* Nesting - These models can be nested recursively to create a hierarchy
|
||||||
|
@ -218,7 +218,7 @@
|
|||||||
|
|
||||||
#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415))
|
#### Custom Scripts ([#3415](https://github.com/netbox-community/netbox/issues/3415))
|
||||||
|
|
||||||
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/additional-features/custom-scripts/) for more detail.
|
Custom scripts allow for the execution of arbitrary code via the NetBox UI. They can be used to automatically create, manipulate, or clean up objects or perform other tasks within NetBox. Scripts are defined as Python files which contain one or more subclasses of `extras.scripts.Script`. Variable fields can be defined within scripts, which render as form fields within the web UI to prompt the user for input data. Scripts are executed and information is logged via the web UI. Please see [the docs](https://netbox.readthedocs.io/en/stable/customization/custom-scripts/) for more detail.
|
||||||
|
|
||||||
Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release.
|
Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release.
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ CUSTOM_VALIDATORS = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
CustomValidator can also be subclassed to enforce more complex logic by overriding its `validate()` method. See the [custom validation](../additional-features/custom-validation.md) documentation for more details.
|
CustomValidator can also be subclassed to enforce more complex logic by overriding its `validate()` method. See the [custom validation](../customization/custom-validation.md) documentation for more details.
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
13
mkdocs.yml
13
mkdocs.yml
@ -59,19 +59,20 @@ nav:
|
|||||||
- Circuits: 'core-functionality/circuits.md'
|
- Circuits: 'core-functionality/circuits.md'
|
||||||
- Power Tracking: 'core-functionality/power.md'
|
- Power Tracking: 'core-functionality/power.md'
|
||||||
- Tenancy: 'core-functionality/tenancy.md'
|
- Tenancy: 'core-functionality/tenancy.md'
|
||||||
|
- Customization:
|
||||||
|
- Custom Fields: 'customization/custom-fields.md'
|
||||||
|
- Custom Validation: 'customization/custom-validation.md'
|
||||||
|
- Custom Links: 'customization/custom-links.md'
|
||||||
|
- Export Templates: 'customization/export-templates.md'
|
||||||
|
- Custom Scripts: 'customization/custom-scripts.md'
|
||||||
|
- Reports: 'customization/reports.md'
|
||||||
- Additional Features:
|
- Additional Features:
|
||||||
- Caching: 'additional-features/caching.md'
|
- Caching: 'additional-features/caching.md'
|
||||||
- Change Logging: 'additional-features/change-logging.md'
|
- Change Logging: 'additional-features/change-logging.md'
|
||||||
- Context Data: 'models/extras/configcontext.md'
|
- Context Data: 'models/extras/configcontext.md'
|
||||||
- Custom Fields: 'additional-features/custom-fields.md'
|
|
||||||
- Custom Validation: 'additional-features/custom-validation.md'
|
|
||||||
- Custom Links: 'additional-features/custom-links.md'
|
|
||||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
|
||||||
- Export Templates: 'additional-features/export-templates.md'
|
|
||||||
- Journaling: 'additional-features/journaling.md'
|
- Journaling: 'additional-features/journaling.md'
|
||||||
- NAPALM: 'additional-features/napalm.md'
|
- NAPALM: 'additional-features/napalm.md'
|
||||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||||
- Reports: 'additional-features/reports.md'
|
|
||||||
- Tags: 'models/extras/tag.md'
|
- Tags: 'models/extras/tag.md'
|
||||||
- Webhooks: 'additional-features/webhooks.md'
|
- Webhooks: 'additional-features/webhooks.md'
|
||||||
- Plugins:
|
- Plugins:
|
||||||
|
@ -1,200 +1,6 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField
|
from .models import JobResult
|
||||||
from utilities.utils import content_type_name
|
|
||||||
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
|
||||||
from .utils import FeatureQuery
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Webhooks
|
|
||||||
#
|
|
||||||
|
|
||||||
class WebhookForm(forms.ModelForm):
|
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('webhooks')
|
|
||||||
)
|
|
||||||
payload_url = LaxURLField(
|
|
||||||
label='URL'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Webhook
|
|
||||||
exclude = ()
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Webhook)
|
|
||||||
class WebhookAdmin(admin.ModelAdmin):
|
|
||||||
list_display = [
|
|
||||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
|
||||||
'ssl_verification',
|
|
||||||
]
|
|
||||||
list_filter = [
|
|
||||||
'enabled', 'type_create', 'type_update', 'type_delete', 'content_types',
|
|
||||||
]
|
|
||||||
form = WebhookForm
|
|
||||||
fieldsets = (
|
|
||||||
(None, {
|
|
||||||
'fields': ('name', 'content_types', 'enabled')
|
|
||||||
}),
|
|
||||||
('Events', {
|
|
||||||
'fields': ('type_create', 'type_update', 'type_delete')
|
|
||||||
}),
|
|
||||||
('HTTP Request', {
|
|
||||||
'fields': (
|
|
||||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
|
||||||
),
|
|
||||||
'classes': ('monospace',)
|
|
||||||
}),
|
|
||||||
('SSL', {
|
|
||||||
'fields': ('ssl_verification', 'ca_file_path')
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
def models(self, obj):
|
|
||||||
return ', '.join([ct.name for ct in obj.content_types.all()])
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomFieldForm(forms.ModelForm):
|
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('custom_fields')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomField
|
|
||||||
exclude = []
|
|
||||||
widgets = {
|
|
||||||
'default': forms.TextInput(),
|
|
||||||
'validation_regex': forms.Textarea(
|
|
||||||
attrs={
|
|
||||||
'cols': 80,
|
|
||||||
'rows': 3,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomField)
|
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
|
||||||
actions = None
|
|
||||||
form = CustomFieldForm
|
|
||||||
list_display = [
|
|
||||||
'name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description',
|
|
||||||
]
|
|
||||||
list_filter = [
|
|
||||||
'type', 'required', 'content_types',
|
|
||||||
]
|
|
||||||
fieldsets = (
|
|
||||||
('Custom Field', {
|
|
||||||
'fields': ('type', 'name', 'weight', 'label', 'description', 'required', 'default', 'filter_logic')
|
|
||||||
}),
|
|
||||||
('Assignment', {
|
|
||||||
'description': 'A custom field must be assigned to one or more object types.',
|
|
||||||
'fields': ('content_types',)
|
|
||||||
}),
|
|
||||||
('Validation Rules', {
|
|
||||||
'fields': ('validation_minimum', 'validation_maximum', 'validation_regex'),
|
|
||||||
'classes': ('monospace',)
|
|
||||||
}),
|
|
||||||
('Choices', {
|
|
||||||
'description': 'A selection field must have two or more choices assigned to it.',
|
|
||||||
'fields': ('choices',)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
def models(self, obj):
|
|
||||||
ct_names = [content_type_name(ct) for ct in obj.content_types.all()]
|
|
||||||
return mark_safe('<br/>'.join(ct_names))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom links
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomLinkForm(forms.ModelForm):
|
|
||||||
content_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomLink
|
|
||||||
exclude = []
|
|
||||||
widgets = {
|
|
||||||
'link_text': forms.Textarea,
|
|
||||||
'link_url': forms.Textarea,
|
|
||||||
}
|
|
||||||
help_texts = {
|
|
||||||
'weight': 'A numeric weight to influence the ordering of this link among its peers. Lower weights appear '
|
|
||||||
'first in a list.',
|
|
||||||
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
|
|
||||||
'Links which render as empty text will not be displayed.',
|
|
||||||
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomLink)
|
|
||||||
class CustomLinkAdmin(admin.ModelAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
('Custom Link', {
|
|
||||||
'fields': ('content_type', 'name', 'group_name', 'weight', 'button_class', 'new_window')
|
|
||||||
}),
|
|
||||||
('Templates', {
|
|
||||||
'fields': ('link_text', 'link_url'),
|
|
||||||
'classes': ('monospace',)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
list_display = [
|
|
||||||
'name', 'content_type', 'group_name', 'weight',
|
|
||||||
]
|
|
||||||
list_filter = [
|
|
||||||
'content_type',
|
|
||||||
]
|
|
||||||
form = CustomLinkForm
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Export templates
|
|
||||||
#
|
|
||||||
|
|
||||||
class ExportTemplateForm(forms.ModelForm):
|
|
||||||
content_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ExportTemplate
|
|
||||||
exclude = []
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ExportTemplate)
|
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
('Export Template', {
|
|
||||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment')
|
|
||||||
}),
|
|
||||||
('Content', {
|
|
||||||
'fields': ('template_code',),
|
|
||||||
'classes': ('monospace',)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
list_display = [
|
|
||||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
|
||||||
]
|
|
||||||
list_filter = [
|
|
||||||
'content_type',
|
|
||||||
]
|
|
||||||
form = ExportTemplateForm
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField,
|
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField,
|
||||||
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
|
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm,
|
||||||
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||||
|
StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
from .models import *
|
||||||
from .utils import FeatureQuery
|
from .utils import FeatureQuery
|
||||||
|
|
||||||
|
|
||||||
@ -21,6 +22,379 @@ from .utils import FeatureQuery
|
|||||||
# Custom fields
|
# Custom fields
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomField
|
||||||
|
fields = '__all__'
|
||||||
|
fieldsets = (
|
||||||
|
('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')),
|
||||||
|
('Assigned Models', ('content_types',)),
|
||||||
|
('Behavior', ('filter_logic',)),
|
||||||
|
('Values', ('default', 'choices')),
|
||||||
|
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldCSVForm(CSVModelForm):
|
||||||
|
content_types = CSVMultipleContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
|
help_text="One or more assigned object types"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomField
|
||||||
|
fields = (
|
||||||
|
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
|
||||||
|
'weight',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=CustomField.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
required = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
field_groups = [
|
||||||
|
['type', 'content_types'],
|
||||||
|
['weight', 'required'],
|
||||||
|
]
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields')
|
||||||
|
)
|
||||||
|
type = forms.MultipleChoiceField(
|
||||||
|
choices=CustomFieldTypeChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2Multiple()
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
required = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom links
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_links')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomLink
|
||||||
|
fields = '__all__'
|
||||||
|
fieldsets = (
|
||||||
|
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
|
||||||
|
('Templates', ('link_text', 'link_url')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkCSVForm(CSVModelForm):
|
||||||
|
content_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_links'),
|
||||||
|
help_text="Assigned object type"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomLink
|
||||||
|
fields = (
|
||||||
|
'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=CustomLink.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
new_window = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
button_class = forms.ChoiceField(
|
||||||
|
choices=CustomLinkButtonClassChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
field_groups = [
|
||||||
|
['content_type'],
|
||||||
|
['weight', 'new_window'],
|
||||||
|
]
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields')
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
new_window = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Export templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_links')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExportTemplate
|
||||||
|
fields = '__all__'
|
||||||
|
fieldsets = (
|
||||||
|
('Custom Link', ('name', 'content_type', 'description')),
|
||||||
|
('Template', ('template_code',)),
|
||||||
|
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateCSVForm(CSVModelForm):
|
||||||
|
content_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('export_templates'),
|
||||||
|
help_text="Assigned object type"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExportTemplate
|
||||||
|
fields = (
|
||||||
|
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ExportTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
mime_type = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
file_extension = forms.CharField(
|
||||||
|
max_length=15,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
as_attachment = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description', 'mime_type', 'file_extension']
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
field_groups = [
|
||||||
|
['content_type', 'mime_type'],
|
||||||
|
['file_extension', 'as_attachment'],
|
||||||
|
]
|
||||||
|
content_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields')
|
||||||
|
)
|
||||||
|
mime_type = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
file_extension = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
as_attachment = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('webhooks')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
fields = '__all__'
|
||||||
|
fieldsets = (
|
||||||
|
('Webhook', ('name', 'enabled')),
|
||||||
|
('Assigned Models', ('content_types',)),
|
||||||
|
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||||
|
('HTTP Request', (
|
||||||
|
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||||
|
)),
|
||||||
|
('SSL', ('ssl_verification', 'ca_file_path')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookCSVForm(CSVModelForm):
|
||||||
|
content_types = CSVMultipleContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('webhooks'),
|
||||||
|
help_text="One or more assigned object types"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
fields = (
|
||||||
|
'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
|
||||||
|
'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
|
||||||
|
'ca_file_path'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Webhook.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
type_create = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
type_update = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
type_delete = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
http_method = forms.ChoiceField(
|
||||||
|
choices=WebhookHttpMethodChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
payload_url = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
ssl_verification = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
secret = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
ca_file_path = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['secret', 'ca_file_path']
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
field_groups = [
|
||||||
|
['content_types', 'http_method'],
|
||||||
|
['enabled', 'type_create', 'type_update', 'type_delete'],
|
||||||
|
]
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('custom_fields')
|
||||||
|
)
|
||||||
|
http_method = forms.MultipleChoiceField(
|
||||||
|
choices=WebhookHttpMethodChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2Multiple()
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type_create = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type_update = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type_delete = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom field models
|
||||||
|
#
|
||||||
|
|
||||||
class CustomFieldsMixin:
|
class CustomFieldsMixin:
|
||||||
"""
|
"""
|
||||||
Extend a Form to include custom field support.
|
Extend a Form to include custom field support.
|
||||||
|
51
netbox/extras/migrations/0061_extras_change_logging.py
Normal file
51
netbox/extras/migrations/0061_extras_change_logging.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0060_customlink_button_class'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -6,11 +6,12 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.validators import RegexValidator, ValidationError
|
from django.core.validators import RegexValidator, ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery, extras_features
|
||||||
from netbox.models import BigIDModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
CSVChoiceField, DatePicker, LaxURLField, StaticSelect2Multiple, StaticSelect2, add_blank_choice,
|
CSVChoiceField, DatePicker, LaxURLField, StaticSelect2Multiple, StaticSelect2, add_blank_choice,
|
||||||
)
|
)
|
||||||
@ -29,7 +30,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|||||||
return self.get_queryset().filter(content_types=content_type)
|
return self.get_queryset().filter(content_types=content_type)
|
||||||
|
|
||||||
|
|
||||||
class CustomField(BigIDModel):
|
@extras_features('webhooks')
|
||||||
|
class CustomField(ChangeLoggedModel):
|
||||||
content_types = models.ManyToManyField(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
related_name='custom_fields',
|
related_name='custom_fields',
|
||||||
@ -114,6 +116,9 @@ class CustomField(BigIDModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.label or self.name.replace('_', ' ').capitalize()
|
return self.label or self.name.replace('_', ' ').capitalize()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:customfield', args=[self.pk])
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -36,7 +36,8 @@ __all__ = (
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
|
||||||
class Webhook(BigIDModel):
|
@extras_features('webhooks')
|
||||||
|
class Webhook(ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||||
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
||||||
@ -129,6 +130,9 @@ class Webhook(BigIDModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:webhook', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -171,7 +175,8 @@ class Webhook(BigIDModel):
|
|||||||
# Custom links
|
# Custom links
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomLink(BigIDModel):
|
@extras_features('webhooks')
|
||||||
|
class CustomLink(ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
|
||||||
code to be rendered with an object as context.
|
code to be rendered with an object as context.
|
||||||
@ -221,12 +226,16 @@ class CustomLink(BigIDModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:customlink', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplate(BigIDModel):
|
@extras_features('webhooks')
|
||||||
|
class ExportTemplate(ChangeLoggedModel):
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -269,6 +278,9 @@ class ExportTemplate(BigIDModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.content_type}: {self.name}"
|
return f"{self.content_type}: {self.name}"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:exporttemplate', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from django.conf import settings
|
|||||||
from utilities.tables import (
|
from utilities.tables import (
|
||||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ToggleColumn,
|
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ToggleColumn,
|
||||||
)
|
)
|
||||||
from .models import ConfigContext, JournalEntry, ObjectChange, Tag, TaggedItem
|
from .models import *
|
||||||
|
|
||||||
CONFIGCONTEXT_ACTIONS = """
|
CONFIGCONTEXT_ACTIONS = """
|
||||||
{% if perms.extras.change_configcontext %}
|
{% if perms.extras.change_configcontext %}
|
||||||
@ -28,6 +28,96 @@ OBJECTCHANGE_REQUEST_ID = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomFieldTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
required = BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = CustomField
|
||||||
|
fields = (
|
||||||
|
'pk', 'name', 'label', 'type', 'required', 'weight', 'default', 'description', 'filter_logic', 'choices',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'label', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom links
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomLinkTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
content_type = ContentTypeColumn()
|
||||||
|
new_window = BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = CustomLink
|
||||||
|
fields = (
|
||||||
|
'pk', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Export templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExportTemplateTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
content_type = ContentTypeColumn()
|
||||||
|
as_attachment = BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = ExportTemplate
|
||||||
|
fields = (
|
||||||
|
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebhookTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
enabled = BooleanColumn()
|
||||||
|
type_create = BooleanColumn()
|
||||||
|
type_update = BooleanColumn()
|
||||||
|
type_delete = BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Webhook
|
||||||
|
fields = (
|
||||||
|
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
|
||||||
|
'secret', 'ssl_validation', 'ca_file_path',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
class TagTable(BaseTable):
|
class TagTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
@ -6,11 +6,163 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import ObjectChangeActionChoices
|
from extras.choices import *
|
||||||
from extras.models import ConfigContext, CustomLink, JournalEntry, ObjectChange, Tag
|
from extras.models import *
|
||||||
from utilities.testing import ViewTestCases, TestCase
|
from utilities.testing import ViewTestCases, TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = CustomField
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
custom_fields = (
|
||||||
|
CustomField(name='field1', label='Field 1', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||||
|
CustomField(name='field2', label='Field 2', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||||
|
CustomField(name='field3', label='Field 3', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||||
|
)
|
||||||
|
for customfield in custom_fields:
|
||||||
|
customfield.save()
|
||||||
|
customfield.content_types.add(site_ct)
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'field_x',
|
||||||
|
'label': 'Field X',
|
||||||
|
'type': 'text',
|
||||||
|
'content_types': [site_ct.pk],
|
||||||
|
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
||||||
|
'default': None,
|
||||||
|
'weight': 200,
|
||||||
|
'required': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,label,type,content_types,weight,filter_logic",
|
||||||
|
"field4,Field 4,text,dcim.site,100,exact",
|
||||||
|
"field5,Field 5,text,dcim.site,100,exact",
|
||||||
|
"field6,Field 6,text,dcim.site,100,exact",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'required': True,
|
||||||
|
'weight': 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = CustomLink
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
CustomLink.objects.bulk_create((
|
||||||
|
CustomLink(name='Custom Link 1', content_type=site_ct, link_text='Link 1', link_url='http://example.com/?1'),
|
||||||
|
CustomLink(name='Custom Link 2', content_type=site_ct, link_text='Link 2', link_url='http://example.com/?2'),
|
||||||
|
CustomLink(name='Custom Link 3', content_type=site_ct, link_text='Link 3', link_url='http://example.com/?3'),
|
||||||
|
))
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Custom Link X',
|
||||||
|
'content_type': site_ct.pk,
|
||||||
|
'weight': 100,
|
||||||
|
'button_class': CustomLinkButtonClassChoices.CLASS_DEFAULT,
|
||||||
|
'link_text': 'Link X',
|
||||||
|
'link_url': 'http://example.com/?x'
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,content_type,weight,button_class,link_text,link_url",
|
||||||
|
"Custom Link 4,dcim.site,100,primary,Link 4,http://exmaple.com/?4",
|
||||||
|
"Custom Link 5,dcim.site,100,primary,Link 5,http://exmaple.com/?5",
|
||||||
|
"Custom Link 6,dcim.site,100,primary,Link 6,http://exmaple.com/?6",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'button_class': CustomLinkButtonClassChoices.CLASS_INFO,
|
||||||
|
'weight': 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = ExportTemplate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}"""
|
||||||
|
ExportTemplate.objects.bulk_create((
|
||||||
|
ExportTemplate(name='Export Template 1', content_type=site_ct, template_code=TEMPLATE_CODE),
|
||||||
|
ExportTemplate(name='Export Template 2', content_type=site_ct, template_code=TEMPLATE_CODE),
|
||||||
|
ExportTemplate(name='Export Template 3', content_type=site_ct, template_code=TEMPLATE_CODE),
|
||||||
|
))
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Export Template X',
|
||||||
|
'content_type': site_ct.pk,
|
||||||
|
'template_code': TEMPLATE_CODE,
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,content_type,template_code",
|
||||||
|
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
||||||
|
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
||||||
|
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'mime_type': 'text/html',
|
||||||
|
'file_extension': 'html',
|
||||||
|
'as_attachment': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = Webhook
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
webhooks = (
|
||||||
|
Webhook(name='Webhook 1', payload_url='http://example.com/?1', type_create=True, http_method='POST'),
|
||||||
|
Webhook(name='Webhook 2', payload_url='http://example.com/?2', type_create=True, http_method='POST'),
|
||||||
|
Webhook(name='Webhook 3', payload_url='http://example.com/?3', type_create=True, http_method='POST'),
|
||||||
|
)
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook.save()
|
||||||
|
webhook.content_types.add(site_ct)
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Webhook X',
|
||||||
|
'content_types': [site_ct.pk],
|
||||||
|
'type_create': False,
|
||||||
|
'type_update': True,
|
||||||
|
'type_delete': True,
|
||||||
|
'payload_url': 'http://example.com/?x',
|
||||||
|
'http_method': 'GET',
|
||||||
|
'http_content_type': 'application/foo',
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"name,content_types,type_create,payload_url,http_method,http_content_type",
|
||||||
|
"Webhook 4,dcim.site,True,http://example.com/?4,GET,application/json",
|
||||||
|
"Webhook 5,dcim.site,True,http://example.com/?5,GET,application/json",
|
||||||
|
"Webhook 6,dcim.site,True,http://example.com/?6,GET,application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'enabled': False,
|
||||||
|
'type_create': False,
|
||||||
|
'type_update': True,
|
||||||
|
'type_delete': True,
|
||||||
|
'http_method': 'GET',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = Tag
|
model = Tag
|
||||||
|
|
||||||
|
@ -1,12 +1,59 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from extras import views
|
from extras import models, views
|
||||||
from extras.models import ConfigContext, JournalEntry, Tag
|
|
||||||
|
|
||||||
|
|
||||||
app_name = 'extras'
|
app_name = 'extras'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
# Custom fields
|
||||||
|
path('custom-fields/', views.CustomFieldListView.as_view(), name='customfield_list'),
|
||||||
|
path('custom-fields/add/', views.CustomFieldEditView.as_view(), name='customfield_add'),
|
||||||
|
path('custom-fields/import/', views.CustomFieldBulkImportView.as_view(), name='customfield_import'),
|
||||||
|
path('custom-fields/edit/', views.CustomFieldBulkEditView.as_view(), name='customfield_bulk_edit'),
|
||||||
|
path('custom-fields/delete/', views.CustomFieldBulkDeleteView.as_view(), name='customfield_bulk_delete'),
|
||||||
|
path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'),
|
||||||
|
path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'),
|
||||||
|
path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
|
||||||
|
path('custom-fields/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customfield_changelog',
|
||||||
|
kwargs={'model': models.CustomField}),
|
||||||
|
|
||||||
|
# Custom links
|
||||||
|
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
|
||||||
|
path('custom-links/add/', views.CustomLinkEditView.as_view(), name='customlink_add'),
|
||||||
|
path('custom-links/import/', views.CustomLinkBulkImportView.as_view(), name='customlink_import'),
|
||||||
|
path('custom-links/edit/', views.CustomLinkBulkEditView.as_view(), name='customlink_bulk_edit'),
|
||||||
|
path('custom-links/delete/', views.CustomLinkBulkDeleteView.as_view(), name='customlink_bulk_delete'),
|
||||||
|
path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'),
|
||||||
|
path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'),
|
||||||
|
path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'),
|
||||||
|
path('custom-links/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog',
|
||||||
|
kwargs={'model': models.CustomLink}),
|
||||||
|
|
||||||
|
# Export templates
|
||||||
|
path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'),
|
||||||
|
path('export-templates/add/', views.ExportTemplateEditView.as_view(), name='exporttemplate_add'),
|
||||||
|
path('export-templates/import/', views.ExportTemplateBulkImportView.as_view(), name='exporttemplate_import'),
|
||||||
|
path('export-templates/edit/', views.ExportTemplateBulkEditView.as_view(), name='exporttemplate_bulk_edit'),
|
||||||
|
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
||||||
|
path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'),
|
||||||
|
path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'),
|
||||||
|
path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'),
|
||||||
|
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
|
||||||
|
kwargs={'model': models.ExportTemplate}),
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||||
|
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
||||||
|
path('webhooks/import/', views.WebhookBulkImportView.as_view(), name='webhook_import'),
|
||||||
|
path('webhooks/edit/', views.WebhookBulkEditView.as_view(), name='webhook_bulk_edit'),
|
||||||
|
path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'),
|
||||||
|
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
|
||||||
|
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
|
||||||
|
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
|
||||||
|
path('webhooks/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='webhook_changelog',
|
||||||
|
kwargs={'model': models.Webhook}),
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
||||||
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),
|
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),
|
||||||
@ -16,7 +63,8 @@ urlpatterns = [
|
|||||||
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
|
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
|
||||||
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||||
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||||
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog',
|
||||||
|
kwargs={'model': models.Tag}),
|
||||||
|
|
||||||
# Config contexts
|
# Config contexts
|
||||||
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||||
@ -26,7 +74,8 @@ urlpatterns = [
|
|||||||
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
|
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
|
||||||
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||||
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||||
path('config-contexts/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog', kwargs={'model': ConfigContext}),
|
path('config-contexts/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog',
|
||||||
|
kwargs={'model': models.ConfigContext}),
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||||
@ -40,7 +89,8 @@ urlpatterns = [
|
|||||||
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
|
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
|
||||||
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
|
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
|
||||||
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
|
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
|
||||||
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', kwargs={'model': JournalEntry}),
|
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog',
|
||||||
|
kwargs={'model': models.JournalEntry}),
|
||||||
|
|
||||||
# Change logging
|
# Change logging
|
||||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||||
|
@ -15,11 +15,183 @@ from utilities.utils import copy_safe_request, count_related, shallow_compare_di
|
|||||||
from utilities.views import ContentTypePermissionRequiredMixin
|
from utilities.views import ContentTypePermissionRequiredMixin
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .choices import JobResultStatusChoices
|
from .choices import JobResultStatusChoices
|
||||||
from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
|
from .models import *
|
||||||
from .reports import get_report, get_reports, run_report
|
from .reports import get_report, get_reports, run_report
|
||||||
from .scripts import get_scripts, run_script
|
from .scripts import get_scripts, run_script
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomFieldListView(generic.ObjectListView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
|
filterset_form = forms.CustomFieldFilterForm
|
||||||
|
table = tables.CustomFieldTable
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldView(generic.ObjectView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldEditView(generic.ObjectEditView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
model_form = forms.CustomFieldForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
model_form = forms.CustomFieldCSVForm
|
||||||
|
table = tables.CustomFieldTable
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
|
table = tables.CustomFieldTable
|
||||||
|
form = forms.CustomFieldBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = CustomField.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
|
table = tables.CustomFieldTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom links
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomLinkListView(generic.ObjectListView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
filterset = filtersets.CustomLinkFilterSet
|
||||||
|
filterset_form = forms.CustomLinkFilterForm
|
||||||
|
table = tables.CustomLinkTable
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkView(generic.ObjectView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkEditView(generic.ObjectEditView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
model_form = forms.CustomLinkForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
model_form = forms.CustomLinkCSVForm
|
||||||
|
table = tables.CustomLinkTable
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
filterset = filtersets.CustomLinkFilterSet
|
||||||
|
table = tables.CustomLinkTable
|
||||||
|
form = forms.CustomLinkBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = CustomLink.objects.all()
|
||||||
|
filterset = filtersets.CustomLinkFilterSet
|
||||||
|
table = tables.CustomLinkTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Export templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExportTemplateListView(generic.ObjectListView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
filterset = filtersets.ExportTemplateFilterSet
|
||||||
|
filterset_form = forms.ExportTemplateFilterForm
|
||||||
|
table = tables.ExportTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateView(generic.ObjectView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateEditView(generic.ObjectEditView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
model_form = forms.ExportTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
model_form = forms.ExportTemplateCSVForm
|
||||||
|
table = tables.ExportTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
filterset = filtersets.ExportTemplateFilterSet
|
||||||
|
table = tables.ExportTemplateTable
|
||||||
|
form = forms.ExportTemplateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = ExportTemplate.objects.all()
|
||||||
|
filterset = filtersets.ExportTemplateFilterSet
|
||||||
|
table = tables.ExportTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Webhooks
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebhookListView(generic.ObjectListView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
filterset = filtersets.WebhookFilterSet
|
||||||
|
filterset_form = forms.WebhookFilterForm
|
||||||
|
table = tables.WebhookTable
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookView(generic.ObjectView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookEditView(generic.ObjectEditView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
model_form = forms.WebhookForm
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
model_form = forms.WebhookCSVForm
|
||||||
|
table = tables.WebhookTable
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
filterset = filtersets.WebhookFilterSet
|
||||||
|
table = tables.WebhookTable
|
||||||
|
form = forms.WebhookBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = Webhook.objects.all()
|
||||||
|
filterset = filtersets.WebhookFilterSet
|
||||||
|
table = tables.WebhookTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
120
netbox/templates/extras/customfield.html
Normal file
120
netbox/templates/extras/customfield.html
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'extras:customfield_list' %}">Custom Fields</a></li>
|
||||||
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Custom Field
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Label</th>
|
||||||
|
<td>{{ object.label|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Type</th>
|
||||||
|
<td>{{ object.get_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Required</th>
|
||||||
|
<td>
|
||||||
|
{% if object.required %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Weight</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Values
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Default Value</th>
|
||||||
|
<td>{{ object.default }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Choices</th>
|
||||||
|
<td>{{ object.choices|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Filter Logic</th>
|
||||||
|
<td>{{ object.get_filter_logic_display }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Assigned Models
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Validation Rules
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Minimum Value</th>
|
||||||
|
<td>{{ object.validation_minimum|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Maximum Value</th>
|
||||||
|
<td>{{ object.validation_maximum|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Regular Expression</th>
|
||||||
|
<td>
|
||||||
|
{% if object.validation_regex %}
|
||||||
|
<code>{{ object.validation_regex }}</code>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
74
netbox/templates/extras/customlink.html
Normal file
74
netbox/templates/extras/customlink.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'extras:customlink_list' %}">Custom Links</a></li>
|
||||||
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Custom Link
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Content Type</th>
|
||||||
|
<td>{{ object.content_type }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Group Name</th>
|
||||||
|
<td>{{ object.group_name|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Weight</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Button Class</th>
|
||||||
|
<td>{{ object.get_button_class_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">New Window</th>
|
||||||
|
<td>
|
||||||
|
{% if object.new_window %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Link Text
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.link_text }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Link URL
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.link_url }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
66
netbox/templates/extras/exporttemplate.html
Normal file
66
netbox/templates/extras/exporttemplate.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'extras:exporttemplate_list' %}">Export Templates</a></li>
|
||||||
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Export Template
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Content Type</th>
|
||||||
|
<td>{{ object.content_type }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">MIME Type</th>
|
||||||
|
<td>{{ object.mime_type|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">File Extension</th>
|
||||||
|
<td>{{ object.file_extension|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Attachment</th>
|
||||||
|
<td>
|
||||||
|
{% if object.as_attachment %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Template
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.template_code }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
165
netbox/templates/extras/webhook.html
Normal file
165
netbox/templates/extras/webhook.html
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'extras:webhook_list' %}">Webhooks</a></li>
|
||||||
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Webhook
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enabled</th>
|
||||||
|
<td>
|
||||||
|
{% if object.enabled %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Events
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Create</th>
|
||||||
|
<td>
|
||||||
|
{% if object.type_create %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Update</th>
|
||||||
|
<td>
|
||||||
|
{% if object.type_create %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Delete</th>
|
||||||
|
<td>
|
||||||
|
{% if object.type_create %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
HTTP Request
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">HTTP Method</th>
|
||||||
|
<td>{{ object.get_http_method_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Payload URL</th>
|
||||||
|
<td><code>{{ object.payload_url }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">HTTP Content Type</th>
|
||||||
|
<td>{{ object.http_content_type }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Secret</th>
|
||||||
|
<td>{{ object.secret|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
SSL
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">SSL Verification</th>
|
||||||
|
<td>
|
||||||
|
{% if object.ssl_verification %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">CA File Path</th>
|
||||||
|
<td>
|
||||||
|
{% if object.ca_file_path %}
|
||||||
|
<code>{{ object.ca_file_path }}</code>
|
||||||
|
{% else %}
|
||||||
|
&mdash
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Assigned Models
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Additional Headers
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.additional_headers }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Body Template
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.body_template }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -6,8 +6,9 @@ from io import StringIO
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.forms import BoundField
|
from django.forms import BoundField
|
||||||
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
|
from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -28,6 +29,7 @@ __all__ = (
|
|||||||
'CSVContentTypeField',
|
'CSVContentTypeField',
|
||||||
'CSVDataField',
|
'CSVDataField',
|
||||||
'CSVModelChoiceField',
|
'CSVModelChoiceField',
|
||||||
|
'CSVMultipleContentTypeField',
|
||||||
'CSVTypedChoiceField',
|
'CSVTypedChoiceField',
|
||||||
'DynamicModelChoiceField',
|
'DynamicModelChoiceField',
|
||||||
'DynamicModelMultipleChoiceField',
|
'DynamicModelMultipleChoiceField',
|
||||||
@ -281,6 +283,20 @@ class CSVContentTypeField(CSVModelChoiceField):
|
|||||||
raise forms.ValidationError(f'Invalid object type')
|
raise forms.ValidationError(f'Invalid object type')
|
||||||
|
|
||||||
|
|
||||||
|
class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
|
||||||
|
STATIC_CHOICES = True
|
||||||
|
|
||||||
|
# TODO: Improve validation of selected ContentTypes
|
||||||
|
def prepare_value(self, value):
|
||||||
|
if type(value) is str:
|
||||||
|
ct_filter = Q()
|
||||||
|
for name in value.split(','):
|
||||||
|
app_label, model = name.split('.')
|
||||||
|
ct_filter |= Q(app_label=app_label, model=model)
|
||||||
|
return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
|
||||||
|
return super().prepare_value(value)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Expansion fields
|
# Expansion fields
|
||||||
#
|
#
|
||||||
|
@ -88,7 +88,7 @@ def export_button(context, content_type=None):
|
|||||||
user = context['request'].user
|
user = context['request'].user
|
||||||
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type)
|
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type)
|
||||||
if user.is_staff and user.has_perm('extras.add_exporttemplate'):
|
if user.is_staff and user.has_perm('extras.add_exporttemplate'):
|
||||||
add_exporttemplate_link = f"{reverse('admin:extras_exporttemplate_add')}?content_type={content_type.pk}"
|
add_exporttemplate_link = f"{reverse('extras:exporttemplate_add')}?content_type={content_type.pk}"
|
||||||
else:
|
else:
|
||||||
export_templates = []
|
export_templates = []
|
||||||
|
|
||||||
|
@ -287,6 +287,19 @@ OTHER_MENU = Menu(
|
|||||||
add_url=None, import_url=None),
|
add_url=None, import_url=None),
|
||||||
MenuItem(label="Journal Entries",
|
MenuItem(label="Journal Entries",
|
||||||
url="extras:journalentry_list", add_url=None, import_url=None),
|
url="extras:journalentry_list", add_url=None, import_url=None),
|
||||||
|
MenuItem(label="Webhooks", url="extras:webhook_list",
|
||||||
|
add_url="extras:webhook_add", import_url="extras:webhook_import"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label="Customization",
|
||||||
|
items=(
|
||||||
|
MenuItem(label="Custom Fields", url="extras:customfield_list",
|
||||||
|
add_url="extras:customfield_add", import_url="extras:customfield_import"),
|
||||||
|
MenuItem(label="Custom Links", url="extras:customlink_list",
|
||||||
|
add_url="extras:customlink_add", import_url="extras:customlink_import"),
|
||||||
|
MenuItem(label="Export Templates", url="extras:exporttemplate_list",
|
||||||
|
add_url="extras:exporttemplate_add", import_url="extras:exporttemplate_import"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
@ -109,12 +109,12 @@ class ModelTestCase(TestCase):
|
|||||||
# Handle ManyToManyFields
|
# Handle ManyToManyFields
|
||||||
if value and type(field) in (ManyToManyField, TaggableManager):
|
if value and type(field) in (ManyToManyField, TaggableManager):
|
||||||
|
|
||||||
if field.related_model is ContentType:
|
if field.related_model is ContentType and api:
|
||||||
model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value])
|
model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value])
|
||||||
else:
|
else:
|
||||||
model_dict[key] = sorted([obj.pk for obj in value])
|
model_dict[key] = sorted([obj.pk for obj in value])
|
||||||
|
|
||||||
if api:
|
elif api:
|
||||||
|
|
||||||
# Replace ContentType numeric IDs with <app_label>.<model>
|
# Replace ContentType numeric IDs with <app_label>.<model>
|
||||||
if type(getattr(instance, key)) is ContentType:
|
if type(getattr(instance, key)) is ContentType:
|
||||||
|
Loading…
Reference in New Issue
Block a user