mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #9006: Enable custom fields, custom links, and tags for journal entries
This commit is contained in:
@ -146,6 +146,7 @@ Where it is desired to limit the range of available VLANs within a group, users
|
|||||||
* [#8572](https://github.com/netbox-community/netbox/issues/8572) - Add a `pre_run()` method for reports
|
* [#8572](https://github.com/netbox-community/netbox/issues/8572) - Add a `pre_run()` method for reports
|
||||||
* [#8593](https://github.com/netbox-community/netbox/issues/8593) - Add a `link` field for contacts
|
* [#8593](https://github.com/netbox-community/netbox/issues/8593) - Add a `link` field for contacts
|
||||||
* [#8649](https://github.com/netbox-community/netbox/issues/8649) - Enable customization of configuration module using `NETBOX_CONFIGURATION` environment variable
|
* [#8649](https://github.com/netbox-community/netbox/issues/8649) - Enable customization of configuration module using `NETBOX_CONFIGURATION` environment variable
|
||||||
|
* [#9006](https://github.com/netbox-community/netbox/issues/9006) - Enable custom fields, custom links, and tags for journal entries
|
||||||
|
|
||||||
### Bug Fixes (From Beta2)
|
### Bug Fixes (From Beta2)
|
||||||
|
|
||||||
@ -207,11 +208,13 @@ Where it is desired to limit the range of available VLANs within a group, users
|
|||||||
* Added `data_type` and `object_type` fields
|
* Added `data_type` and `object_type` fields
|
||||||
* extras.CustomLink
|
* extras.CustomLink
|
||||||
* Added `enabled` field
|
* Added `enabled` field
|
||||||
|
* extras.JournalEntry
|
||||||
|
* Added `custom_fields` and `tags` fields
|
||||||
* ipam.ASN
|
* ipam.ASN
|
||||||
* Added `provider_count` field
|
* Added `provider_count` field
|
||||||
* ipam.VLANGroup
|
* ipam.VLANGroup
|
||||||
* Added the `/availables-vlans/` endpoint
|
* Added the `/availables-vlans/` endpoint
|
||||||
* Added the `min_vid` and `max_vid` fields
|
* Added `min_vid` and `max_vid` fields
|
||||||
* tenancy.Contact
|
* tenancy.Contact
|
||||||
* Added `link` field
|
* Added `link` field
|
||||||
* virtualization.VMInterface
|
* virtualization.VMInterface
|
||||||
|
@ -14,7 +14,7 @@ from extras.models import *
|
|||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.exceptions import SerializerNotFound
|
from netbox.api.exceptions import SerializerNotFound
|
||||||
from netbox.api.serializers import BaseModelSerializer, ValidatedModelSerializer
|
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
@ -200,7 +200,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
# Journal entries
|
# Journal entries
|
||||||
#
|
#
|
||||||
|
|
||||||
class JournalEntrySerializer(ValidatedModelSerializer):
|
class JournalEntrySerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||||
assigned_object_type = ContentTypeField(
|
assigned_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
@ -221,7 +221,7 @@ class JournalEntrySerializer(ValidatedModelSerializer):
|
|||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||||
'created_by', 'kind', 'comments',
|
'created_by', 'kind', 'comments', 'tags', 'custom_fields',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
@ -134,11 +134,7 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
|||||||
return queryset.filter(name__icontains=value)
|
return queryset.filter(name__icontains=value)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
|
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
|
||||||
method='search',
|
|
||||||
label='Search',
|
|
||||||
)
|
|
||||||
created = django_filters.DateTimeFromToRangeFilter()
|
created = django_filters.DateTimeFromToRangeFilter()
|
||||||
assigned_object_type = ContentTypeFilter()
|
assigned_object_type = ContentTypeFilter()
|
||||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
@ -7,10 +7,12 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
|
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker,
|
add_blank_choice, APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeChoiceField,
|
||||||
DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField, StaticSelect, BOOLEAN_WITH_BLANK_CHOICES,
|
ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField,
|
||||||
|
StaticSelect, TagFilterField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@ -237,10 +239,10 @@ class LocalConfigContextFilterForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryFilterForm(FilterForm):
|
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'tag')),
|
||||||
('Creation', ('created_before', 'created_after', 'created_by_id')),
|
('Creation', ('created_before', 'created_after', 'created_by_id')),
|
||||||
('Attributes', ('assigned_object_type_id', 'kind'))
|
('Attributes', ('assigned_object_type_id', 'kind'))
|
||||||
)
|
)
|
||||||
@ -275,6 +277,7 @@ class JournalEntryFilterForm(FilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeFilterForm(FilterForm):
|
class ObjectChangeFilterForm(FilterForm):
|
||||||
|
@ -5,6 +5,7 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
|
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
|
||||||
@ -219,18 +220,17 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
class JournalEntryForm(NetBoxModelForm):
|
||||||
comments = CommentField()
|
|
||||||
|
|
||||||
kind = forms.ChoiceField(
|
kind = forms.ChoiceField(
|
||||||
choices=add_blank_choice(JournalEntryKindChoices),
|
choices=add_blank_choice(JournalEntryKindChoices),
|
||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
|
fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
|
||||||
widgets = {
|
widgets = {
|
||||||
'assigned_object_type': forms.HiddenInput,
|
'assigned_object_type': forms.HiddenInput,
|
||||||
'assigned_object_id': forms.HiddenInput,
|
'assigned_object_id': forms.HiddenInput,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from extras import filtersets, models
|
from extras import filtersets, models
|
||||||
|
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
||||||
from netbox.graphql.types import BaseObjectType, ObjectType
|
from netbox.graphql.types import BaseObjectType, ObjectType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -54,7 +55,7 @@ class ImageAttachmentType(BaseObjectType):
|
|||||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryType(ObjectType):
|
class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.JournalEntry
|
model = models.JournalEntry
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import django.core.serializers.json
|
||||||
|
from django.db import migrations, models
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0072_created_datetimefield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='journalentry',
|
||||||
|
name='custom_field_data',
|
||||||
|
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='journalentry',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -19,7 +19,9 @@ from extras.constants import *
|
|||||||
from extras.conditions import ConditionSet
|
from extras.conditions import ConditionSet
|
||||||
from extras.utils import FeatureQuery, image_upload
|
from extras.utils import FeatureQuery, image_upload
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import ExportTemplatesMixin, JobResultsMixin, WebhooksMixin
|
from netbox.models.features import (
|
||||||
|
CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
|
||||||
|
)
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import render_jinja2
|
from utilities.utils import render_jinja2
|
||||||
|
|
||||||
@ -419,7 +421,7 @@ class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
|||||||
return objectchange
|
return objectchange
|
||||||
|
|
||||||
|
|
||||||
class JournalEntry(WebhooksMixin, ChangeLoggedModel):
|
class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
|
||||||
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
|
||||||
|
@ -12,7 +12,6 @@ __all__ = (
|
|||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
'JournalEntryTable',
|
'JournalEntryTable',
|
||||||
'ObjectChangeTable',
|
'ObjectChangeTable',
|
||||||
'ObjectJournalTable',
|
|
||||||
'TaggedItemTable',
|
'TaggedItemTable',
|
||||||
'TagTable',
|
'TagTable',
|
||||||
'WebhookTable',
|
'WebhookTable',
|
||||||
@ -210,25 +209,11 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectJournalTable(NetBoxTable):
|
class JournalEntryTable(NetBoxTable):
|
||||||
"""
|
|
||||||
Used for displaying a set of JournalEntries within the context of a single object.
|
|
||||||
"""
|
|
||||||
created = tables.DateTimeColumn(
|
created = tables.DateTimeColumn(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
format=settings.SHORT_DATETIME_FORMAT
|
format=settings.SHORT_DATETIME_FORMAT
|
||||||
)
|
)
|
||||||
kind = columns.ChoiceFieldColumn()
|
|
||||||
comments = tables.TemplateColumn(
|
|
||||||
template_code='{{ value|markdown|truncatewords_html:50 }}'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
|
||||||
model = JournalEntry
|
|
||||||
fields = ('id', 'created', 'created_by', 'kind', 'comments', 'actions')
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryTable(ObjectJournalTable):
|
|
||||||
assigned_object_type = columns.ContentTypeColumn(
|
assigned_object_type = columns.ContentTypeColumn(
|
||||||
verbose_name='Object type'
|
verbose_name='Object type'
|
||||||
)
|
)
|
||||||
@ -237,13 +222,22 @@ class JournalEntryTable(ObjectJournalTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Object'
|
verbose_name='Object'
|
||||||
)
|
)
|
||||||
|
kind = columns.ChoiceFieldColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
|
comments_short = tables.TemplateColumn(
|
||||||
|
accessor=tables.A('comments'),
|
||||||
|
template_code='{{ value|markdown|truncatewords_html:50 }}',
|
||||||
|
verbose_name='Comments (Short)'
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='extras:journalentry_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments',
|
'pk', 'id', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments',
|
||||||
'actions',
|
'comments_short', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
||||||
|
@ -86,8 +86,10 @@ class ObjectJournalView(View):
|
|||||||
assigned_object_type=content_type,
|
assigned_object_type=content_type,
|
||||||
assigned_object_id=obj.pk
|
assigned_object_id=obj.pk
|
||||||
)
|
)
|
||||||
journalentry_table = tables.ObjectJournalTable(journalentries)
|
journalentry_table = tables.JournalEntryTable(journalentries, user=request.user)
|
||||||
journalentry_table.configure(request)
|
journalentry_table.configure(request)
|
||||||
|
journalentry_table.columns.hide('assigned_object_type')
|
||||||
|
journalentry_table.columns.hide('assigned_object')
|
||||||
|
|
||||||
if request.user.has_perm('extras.add_journalentry'):
|
if request.user.has_perm('extras.add_journalentry'):
|
||||||
form = forms.JournalEntryForm(
|
form = forms.JournalEntryForm(
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Journal Entry
|
Journal Entry
|
||||||
@ -35,8 +35,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-8">
|
<div class="col col-md-7">
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,11 +11,7 @@
|
|||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<h4>New Journal Entry</h4>
|
<h4>New Journal Entry</h4>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form.hidden_fields %}
|
{% render_form form %}
|
||||||
{{ field }}
|
|
||||||
{% endfor %}
|
|
||||||
{% render_field form.kind %}
|
|
||||||
{% render_field form.comments %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-12 text-end my-3">
|
<div class="col col-md-12 text-end my-3">
|
||||||
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
|
Reference in New Issue
Block a user