mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 04:42:22 -06:00
Merge branch 'feature' of https://github.com/netbox-community/netbox into feature
# Conflicts: # netbox/dcim/tables/template_code.py # netbox/netbox/views/__init__.py # netbox/project-static/css/base.css # netbox/templates/base.html # netbox/templates/circuits/circuit.html # netbox/templates/circuits/circuittermination_edit.html # netbox/templates/circuits/inc/circuit_termination.html # netbox/templates/circuits/provider.html # netbox/templates/dcim/device.html # netbox/templates/dcim/device/base.html # netbox/templates/dcim/device_component.html # netbox/templates/dcim/devicetype.html # netbox/templates/dcim/inc/device_napalm_tabs.html # netbox/templates/dcim/rack.html # netbox/templates/dcim/site.html # netbox/templates/dcim/virtualchassis.html # netbox/templates/extras/configcontext.html # netbox/templates/extras/object_journal.html # netbox/templates/extras/tag.html # netbox/templates/generic/object.html # netbox/templates/generic/object_list.html # netbox/templates/home.html # netbox/templates/inc/nav_menu.html # netbox/templates/ipam/aggregate.html # netbox/templates/ipam/ipaddress.html # netbox/templates/ipam/prefix.html # netbox/templates/ipam/vlan.html # netbox/templates/ipam/vlangroup_edit.html # netbox/templates/ipam/vlangroup_vlans.html # netbox/templates/secrets/secret.html # netbox/templates/tenancy/tenant.html # netbox/templates/users/api_tokens.html # netbox/templates/virtualization/cluster.html # netbox/templates/virtualization/vminterface_edit.html # netbox/utilities/forms/fields.py # netbox/utilities/templates/buttons/export.html
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.forms import LaxURLField
|
||||
from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField
|
||||
from utilities.utils import content_type_name
|
||||
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
"""
|
||||
Order the list of available ContentTypes by application
|
||||
"""
|
||||
queryset = field.queryset.order_by('app_label', 'model')
|
||||
field.choices = [(ct.pk, '{} > {}'.format(ct.app_label, ct.name)) for ct in queryset]
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
@@ -18,6 +14,10 @@ def order_content_types(field):
|
||||
#
|
||||
|
||||
class WebhookForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
)
|
||||
payload_url = LaxURLField(
|
||||
label='URL'
|
||||
)
|
||||
@@ -26,12 +26,6 @@ class WebhookForm(forms.ModelForm):
|
||||
model = Webhook
|
||||
exclude = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'content_types' in self.fields:
|
||||
order_content_types(self.fields['content_types'])
|
||||
|
||||
|
||||
@admin.register(Webhook)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
@@ -70,6 +64,10 @@ class WebhookAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
@@ -84,11 +82,6 @@ class CustomFieldForm(forms.ModelForm):
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['content_types'])
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
@@ -119,7 +112,8 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
return ', '.join([ct.name for ct in obj.content_types.all()])
|
||||
ct_names = [content_type_name(ct) for ct in obj.content_types.all()]
|
||||
return mark_safe('<br/>'.join(ct_names))
|
||||
|
||||
|
||||
#
|
||||
@@ -127,6 +121,10 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class CustomLinkForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
@@ -143,13 +141,6 @@ class CustomLinkForm(forms.ModelForm):
|
||||
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(CustomLink)
|
||||
class CustomLinkAdmin(admin.ModelAdmin):
|
||||
@@ -176,24 +167,21 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
||||
#
|
||||
|
||||
class ExportTemplateForm(forms.ModelForm):
|
||||
content_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Export Template', {
|
||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension')
|
||||
'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('template_code',),
|
||||
@@ -201,7 +189,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
})
|
||||
)
|
||||
list_display = [
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension',
|
||||
'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment',
|
||||
]
|
||||
list_filter = [
|
||||
'content_type',
|
||||
|
||||
@@ -4,10 +4,10 @@ from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
|
||||
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
@@ -116,7 +116,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
model = ExportTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_type', 'name', 'description', 'template_code', 'mime_type',
|
||||
'file_extension',
|
||||
'file_extension', 'as_attachment',
|
||||
]
|
||||
|
||||
|
||||
@@ -251,6 +251,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
device_types = SerializedPKRelatedField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
serializer=NestedDeviceTypeSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
roles = SerializedPKRelatedField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
serializer=NestedDeviceRoleSerializer,
|
||||
@@ -298,8 +304,8 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
||||
'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created',
|
||||
'last_updated',
|
||||
'device_types', 'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
|
||||
'data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.forms import DateField, IntegerField, NullBooleanField
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
@@ -90,6 +90,7 @@ class CustomFieldModelFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
@@ -206,6 +207,11 @@ class ConfigContextFilterSet(BaseFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_types',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
@@ -218,6 +218,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
device_types = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
roles = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False
|
||||
@@ -253,8 +257,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'platforms',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
|
||||
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
)
|
||||
|
||||
|
||||
@@ -306,6 +310,11 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label=_('Sites')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
label=_('Device types')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
@@ -376,6 +385,7 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
#
|
||||
|
||||
class JournalEntryForm(BootstrapMixin, forms.ModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
kind = forms.ChoiceField(
|
||||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
|
||||
@@ -14,4 +14,9 @@ class Migration(migrations.Migration):
|
||||
name='site_groups',
|
||||
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_site_groups_+', to='dcim.SiteGroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configcontext',
|
||||
name='device_types',
|
||||
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_device_types_+', to='dcim.DeviceType'),
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0056_sitegroup'),
|
||||
('extras', '0056_extend_configcontext'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -18,6 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('assigned_object_id', models.PositiveIntegerField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('kind', models.CharField(default='info', max_length=30)),
|
||||
('comments', models.TextField()),
|
||||
('assigned_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0058_journalentry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exporttemplate',
|
||||
name='as_attachment',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -56,6 +56,11 @@ class ConfigContext(ChangeLoggedModel):
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
device_types = models.ManyToManyField(
|
||||
to='dcim.DeviceType',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
roles = models.ManyToManyField(
|
||||
to='dcim.DeviceRole',
|
||||
related_name='+',
|
||||
|
||||
@@ -7,13 +7,15 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format, time_format
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.utils import extras_features, FeatureQuery, image_upload
|
||||
from netbox.models import BigIDModel
|
||||
from netbox.models import BigIDModel, ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import render_jinja2
|
||||
|
||||
@@ -151,7 +153,7 @@ class Webhook(BigIDModel):
|
||||
ret = {}
|
||||
data = render_jinja2(self.additional_headers, context)
|
||||
for line in data.splitlines():
|
||||
header, value = line.split(':')
|
||||
header, value = line.split(':', 1)
|
||||
ret[header.strip()] = value.strip()
|
||||
return ret
|
||||
|
||||
@@ -251,6 +253,10 @@ class ExportTemplate(BigIDModel):
|
||||
blank=True,
|
||||
help_text='Extension to append to the rendered filename'
|
||||
)
|
||||
as_attachment = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Download file as attachment"
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -261,7 +267,15 @@ class ExportTemplate(BigIDModel):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return '{}: {}'.format(self.content_type, self.name)
|
||||
return f"{self.content_type}: {self.name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.name.lower() == 'table':
|
||||
raise ValidationError({
|
||||
'name': f'"{self.name}" is a reserved name. Please choose a different name.'
|
||||
})
|
||||
|
||||
def render(self, queryset):
|
||||
"""
|
||||
@@ -290,7 +304,9 @@ class ExportTemplate(BigIDModel):
|
||||
queryset.model._meta.verbose_name_plural,
|
||||
'.{}'.format(self.file_extension) if self.file_extension else ''
|
||||
)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
|
||||
if self.as_attachment:
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
|
||||
return response
|
||||
|
||||
@@ -375,7 +391,7 @@ class ImageAttachment(BigIDModel):
|
||||
# Journal entries
|
||||
#
|
||||
|
||||
class JournalEntry(BigIDModel):
|
||||
class JournalEntry(ChangeLoggedModel):
|
||||
"""
|
||||
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
|
||||
@@ -413,7 +429,10 @@ class JournalEntry(BigIDModel):
|
||||
verbose_name_plural = 'journal entries'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.created} - {self.get_kind_display()}"
|
||||
return f"{date_format(self.created)} - {time_format(self.created)} ({self.get_kind_display()})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:journalentry', args=[self.pk])
|
||||
|
||||
def get_kind_class(self):
|
||||
return JournalEntryKindChoices.CSS_CLASSES.get(self.kind)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
@@ -30,6 +31,9 @@ class Tag(ChangeLoggedModel, TagBase):
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.pk])
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
|
||||
slug = slugify(tag, allow_unicode=True)
|
||||
|
||||
@@ -19,7 +19,10 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
# `device_role` for Device; `role` for VirtualMachine
|
||||
role = getattr(obj, 'device_role', None) or obj.role
|
||||
|
||||
# Virtualization cluster for VirtualMachine
|
||||
# Device type assignment is relevant only for Devices
|
||||
device_type = getattr(obj, 'device_type', None)
|
||||
|
||||
# Cluster assignment is relevant only for VirtualMachines
|
||||
cluster = getattr(obj, 'cluster', None)
|
||||
cluster_group = getattr(cluster, 'group', None)
|
||||
|
||||
@@ -28,14 +31,17 @@ class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
|
||||
# Match against the directly assigned region as well as any parent regions.
|
||||
region = getattr(obj.site, 'region', None)
|
||||
if region:
|
||||
regions = region.get_ancestors(include_self=True)
|
||||
else:
|
||||
regions = []
|
||||
regions = region.get_ancestors(include_self=True) if region else []
|
||||
|
||||
# Match against the directly assigned site group as well as any parent site groups.
|
||||
sitegroup = getattr(obj.site, 'group', None)
|
||||
sitegroups = sitegroup.get_ancestors(include_self=True) if sitegroup else []
|
||||
|
||||
queryset = self.filter(
|
||||
Q(regions__in=regions) | Q(regions=None),
|
||||
Q(site_groups__in=sitegroups) | Q(site_groups=None),
|
||||
Q(sites=obj.site) | Q(sites=None),
|
||||
Q(device_types=device_type) | Q(device_types=None),
|
||||
Q(roles=role) | Q(roles=None),
|
||||
Q(platforms=obj.platform) | Q(platforms=None),
|
||||
Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
|
||||
@@ -108,14 +114,17 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
)
|
||||
|
||||
if self.model._meta.model_name == 'device':
|
||||
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
|
||||
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
|
||||
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
|
||||
region_field = 'site__region'
|
||||
sitegroup_field = 'site__group'
|
||||
|
||||
elif self.model._meta.model_name == 'virtualmachine':
|
||||
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
|
||||
base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
|
||||
region_field = 'cluster__site__region'
|
||||
sitegroup_field = 'cluster__site__group'
|
||||
|
||||
base_query.add(
|
||||
(Q(
|
||||
@@ -127,4 +136,14 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
Q.AND
|
||||
)
|
||||
|
||||
base_query.add(
|
||||
(Q(
|
||||
site_groups__tree_id=OuterRef(f'{sitegroup_field}__tree_id'),
|
||||
site_groups__level__lte=OuterRef(f'{sitegroup_field}__level'),
|
||||
site_groups__lft__lte=OuterRef(f'{sitegroup_field}__lft'),
|
||||
site_groups__rght__gte=OuterRef(f'{sitegroup_field}__rght'),
|
||||
) | Q(site_groups=None)),
|
||||
Q.AND
|
||||
)
|
||||
|
||||
return base_query
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
from cacheops.signals import cache_invalidated, cache_read
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db.models.signals import m2m_changed, pre_delete
|
||||
from django.utils import timezone
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
@@ -22,26 +23,35 @@ def _handle_changed_object(request, sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
# Queue the object for processing once the request completes
|
||||
m2m_changed = False
|
||||
|
||||
# Determine the type of change being made
|
||||
if kwargs.get('created'):
|
||||
action = ObjectChangeActionChoices.ACTION_CREATE
|
||||
elif 'created' in kwargs:
|
||||
action = ObjectChangeActionChoices.ACTION_UPDATE
|
||||
elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
|
||||
# m2m_changed with objects added or removed
|
||||
m2m_changed = True
|
||||
action = ObjectChangeActionChoices.ACTION_UPDATE
|
||||
else:
|
||||
return
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(action)
|
||||
# TODO: Move this to to_objectchange()
|
||||
if hasattr(instance, '_prechange_snapshot'):
|
||||
objectchange.prechange_data = instance._prechange_snapshot
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
if m2m_changed:
|
||||
ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(instance),
|
||||
changed_object_id=instance.pk,
|
||||
request_id=request.id
|
||||
).update(
|
||||
postchange_data=instance.to_objectchange(action).postchange_data
|
||||
)
|
||||
else:
|
||||
objectchange = instance.to_objectchange(action)
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
# Enqueue webhooks
|
||||
enqueue_webhooks(instance, request.user, request.id, action)
|
||||
@@ -55,7 +65,7 @@ def _handle_changed_object(request, sender, instance, **kwargs):
|
||||
# Housekeeping: 0.1% chance of clearing out expired ObjectChanges
|
||||
if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1:
|
||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||
ObjectChange.objects.filter(time__lt=cutoff).delete()
|
||||
ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
|
||||
|
||||
|
||||
def _handle_deleted_object(request, sender, instance, **kwargs):
|
||||
@@ -65,9 +75,6 @@ def _handle_deleted_object(request, sender, instance, **kwargs):
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||
# TODO: Move this to to_objectchange()
|
||||
if hasattr(instance, '_prechange_snapshot'):
|
||||
objectchange.prechange_data = instance._prechange_snapshot
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import django_tables2 as tables
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ToggleColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ToggleColumn,
|
||||
)
|
||||
from .models import ConfigContext, JournalEntry, ObjectChange, Tag, TaggedItem
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CONFIGCONTEXT_ACTIONS = """
|
||||
{% if perms.extras.change_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="mdi mdi-pencil" aria-hidden="true"></i></a>
|
||||
@@ -36,6 +30,9 @@ OBJECTCHANGE_REQUEST_ID = """
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(Tag)
|
||||
|
||||
@@ -45,23 +42,25 @@ class TagTable(BaseTable):
|
||||
|
||||
|
||||
class TaggedItemTable(BaseTable):
|
||||
content_object = tables.TemplateColumn(
|
||||
template_code=TAGGED_ITEM,
|
||||
content_type = ContentTypeColumn(
|
||||
verbose_name='Type'
|
||||
)
|
||||
content_object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
content_type = tables.Column(
|
||||
verbose_name='Type'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TaggedItem
|
||||
fields = ('content_object', 'content_type')
|
||||
fields = ('content_type', 'content_object')
|
||||
|
||||
|
||||
class ConfigContextTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
is_active = BooleanColumn(
|
||||
verbose_name='Active'
|
||||
)
|
||||
@@ -81,7 +80,7 @@ class ObjectChangeTable(BaseTable):
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
action = ChoiceFieldColumn()
|
||||
changed_object_type = tables.Column(
|
||||
changed_object_type = ContentTypeColumn(
|
||||
verbose_name='Type'
|
||||
)
|
||||
object_repr = tables.TemplateColumn(
|
||||
@@ -98,12 +97,30 @@ class ObjectChangeTable(BaseTable):
|
||||
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||
|
||||
|
||||
class JournalEntryTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
class ObjectJournalTable(BaseTable):
|
||||
"""
|
||||
Used for displaying a set of JournalEntries within the context of a single object.
|
||||
"""
|
||||
created = tables.DateTimeColumn(
|
||||
linkify=True,
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
assigned_object_type = tables.Column(
|
||||
kind = ChoiceFieldColumn()
|
||||
comments = tables.TemplateColumn(
|
||||
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = ('created', 'created_by', 'kind', 'comments', 'actions')
|
||||
|
||||
|
||||
class JournalEntryTable(ObjectJournalTable):
|
||||
pk = ToggleColumn()
|
||||
assigned_object_type = ContentTypeColumn(
|
||||
verbose_name='Object type'
|
||||
)
|
||||
assigned_object = tables.Column(
|
||||
@@ -111,32 +128,9 @@ class JournalEntryTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
kind = ChoiceFieldColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry,
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments', 'actions'
|
||||
)
|
||||
|
||||
|
||||
class ObjectJournalTable(BaseTable):
|
||||
"""
|
||||
Used for displaying a set of JournalEntries within the context of a single object.
|
||||
"""
|
||||
created = tables.DateTimeColumn(
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
)
|
||||
kind = ChoiceFieldColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry,
|
||||
buttons=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
fields = ('created', 'created_by', 'kind', 'comments', 'actions')
|
||||
|
||||
@@ -56,19 +56,18 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
# Verify the creation of a new ObjectChange record
|
||||
site = Site.objects.get(name='Site 1')
|
||||
# First OC is the creation; second is the tags update
|
||||
oc_list = ObjectChange.objects.filter(
|
||||
oc = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=site.pk
|
||||
).order_by('pk')
|
||||
self.assertEqual(oc_list[0].changed_object, site)
|
||||
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc_list[0].prechange_data, None)
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
)
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc.prechange_data, None)
|
||||
self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
|
||||
self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
|
||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
|
||||
def test_update_object(self):
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
@@ -93,8 +92,8 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
|
||||
# Verify the creation of a new ObjectChange record
|
||||
site.refresh_from_db()
|
||||
# Get only the most recent OC
|
||||
oc = ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=site.pk
|
||||
@@ -259,17 +258,15 @@ class ChangeLogAPITest(APITestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
# First OC is the creation; second is the tags update
|
||||
oc_list = ObjectChange.objects.filter(
|
||||
oc = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=site.pk
|
||||
).order_by('pk')
|
||||
self.assertEqual(oc_list[0].changed_object, site)
|
||||
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc_list[0].prechange_data, None)
|
||||
self.assertEqual(oc_list[0].postchange_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
)
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
|
||||
self.assertEqual(oc.prechange_data, None)
|
||||
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||
|
||||
def test_update_object(self):
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
@@ -294,11 +291,10 @@ class ChangeLogAPITest(APITestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
# Get only the most recent OC
|
||||
oc = ObjectChange.objects.filter(
|
||||
oc = ObjectChange.objects.get(
|
||||
changed_object_type=ContentType.objects.get_for_model(Site),
|
||||
changed_object_id=site.pk
|
||||
).first()
|
||||
)
|
||||
self.assertEqual(oc.changed_object, site)
|
||||
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
||||
from extras.filters import *
|
||||
from extras.models import *
|
||||
@@ -379,6 +379,14 @@ class ConfigContextTestCase(TestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_types = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-3'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-4'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(device_types)
|
||||
|
||||
device_roles = (
|
||||
DeviceRole(name='Device Role 1', slug='device-role-1'),
|
||||
DeviceRole(name='Device Role 2', slug='device-role-2'),
|
||||
@@ -433,6 +441,7 @@ class ConfigContextTestCase(TestCase):
|
||||
c.regions.set([regions[i]])
|
||||
c.site_groups.set([site_groups[i]])
|
||||
c.sites.set([sites[i]])
|
||||
c.device_types.set([device_types[i]])
|
||||
c.roles.set([device_roles[i]])
|
||||
c.platforms.set([platforms[i]])
|
||||
c.cluster_groups.set([cluster_groups[i]])
|
||||
@@ -475,6 +484,11 @@ class ConfigContextTestCase(TestCase):
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device_type(self):
|
||||
device_types = DeviceType.objects.all()[:2]
|
||||
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_role(self):
|
||||
device_roles = DeviceRole.objects.all()[:2]
|
||||
params = {'role_id': [device_roles[0].pk, device_roles[1].pk]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Site, Region
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@@ -28,7 +28,8 @@ class ConfigContextTest(TestCase):
|
||||
self.devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
self.devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
self.region = Region.objects.create(name="Region")
|
||||
self.site = Site.objects.create(name='Site-1', slug='site-1', region=self.region)
|
||||
self.sitegroup = SiteGroup.objects.create(name="Site Group")
|
||||
self.site = Site.objects.create(name='Site-1', slug='site-1', region=self.region, group=self.sitegroup)
|
||||
self.platform = Platform.objects.create(name="Platform")
|
||||
self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
|
||||
self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
|
||||
@@ -160,6 +161,14 @@ class ConfigContextTest(TestCase):
|
||||
}
|
||||
)
|
||||
region_context.regions.add(self.region)
|
||||
sitegroup_context = ConfigContext.objects.create(
|
||||
name="sitegroup",
|
||||
weight=100,
|
||||
data={
|
||||
"sitegroup": 1
|
||||
}
|
||||
)
|
||||
sitegroup_context.site_groups.add(self.sitegroup)
|
||||
platform_context = ConfigContext.objects.create(
|
||||
name="platform",
|
||||
weight=100,
|
||||
@@ -224,6 +233,14 @@ class ConfigContextTest(TestCase):
|
||||
}
|
||||
)
|
||||
region_context.regions.add(self.region)
|
||||
sitegroup_context = ConfigContext.objects.create(
|
||||
name="sitegroup",
|
||||
weight=100,
|
||||
data={
|
||||
"sitegroup": 1
|
||||
}
|
||||
)
|
||||
sitegroup_context.site_groups.add(self.sitegroup)
|
||||
platform_context = ConfigContext.objects.create(
|
||||
name="platform",
|
||||
weight=100,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras import views
|
||||
from extras.models import ConfigContext, Tag
|
||||
from extras.models import ConfigContext, JournalEntry, Tag
|
||||
|
||||
|
||||
app_name = 'extras'
|
||||
@@ -13,6 +13,7 @@ urlpatterns = [
|
||||
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
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>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||
@@ -36,8 +37,10 @@ urlpatterns = [
|
||||
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
|
||||
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
|
||||
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
|
||||
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>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
|
||||
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', kwargs={'model': JournalEntry}),
|
||||
|
||||
# Change logging
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from django import template
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.db.models import Count, Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import View
|
||||
from django_rq.queues import get_connection
|
||||
from django_tables2 import RequestConfig
|
||||
from rq import Worker
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin
|
||||
from . import filters, forms, tables
|
||||
@@ -35,6 +33,31 @@ class TagListView(generic.ObjectListView):
|
||||
table = tables.TagTable
|
||||
|
||||
|
||||
class TagView(generic.ObjectView):
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
tagged_items = TaggedItem.objects.filter(tag=instance)
|
||||
taggeditem_table = tables.TaggedItemTable(
|
||||
data=tagged_items,
|
||||
orderable=False
|
||||
)
|
||||
paginate_table(taggeditem_table, request)
|
||||
|
||||
object_types = [
|
||||
{
|
||||
'content_type': ContentType.objects.get(pk=ti['content_type']),
|
||||
'item_count': ti['item_count']
|
||||
} for ti in tagged_items.values('content_type').annotate(item_count=Count('pk'))
|
||||
]
|
||||
|
||||
return {
|
||||
'taggeditem_table': taggeditem_table,
|
||||
'tagged_item_count': tagged_items.count(),
|
||||
'object_types': object_types,
|
||||
}
|
||||
|
||||
|
||||
class TagEditView(generic.ObjectEditView):
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagForm
|
||||
@@ -230,23 +253,12 @@ class ObjectChangeLogView(View):
|
||||
data=objectchanges,
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': get_paginate_count(request)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(objectchanges_table)
|
||||
paginate_table(objectchanges_table, request)
|
||||
|
||||
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
|
||||
# fall back to using base.html.
|
||||
if self.base_template is None:
|
||||
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
|
||||
# TODO: This can be removed once an object view has been established for every model.
|
||||
try:
|
||||
template.loader.get_template(self.base_template)
|
||||
except template.TemplateDoesNotExist:
|
||||
self.base_template = 'base.html'
|
||||
|
||||
return render(request, 'extras/object_changelog.html', {
|
||||
'object': obj,
|
||||
@@ -294,6 +306,10 @@ class JournalEntryListView(generic.ObjectListView):
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class JournalEntryView(generic.ObjectView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
|
||||
|
||||
class JournalEntryEditView(generic.ObjectEditView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
model_form = forms.JournalEntryForm
|
||||
@@ -355,17 +371,8 @@ class ObjectJournalView(View):
|
||||
assigned_object_type=content_type,
|
||||
assigned_object_id=obj.pk
|
||||
)
|
||||
journalentry_table = tables.ObjectJournalTable(
|
||||
data=journalentries,
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': get_paginate_count(request)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(journalentry_table)
|
||||
journalentry_table = tables.ObjectJournalTable(journalentries)
|
||||
paginate_table(journalentry_table, request)
|
||||
|
||||
if request.user.has_perm('extras.add_journalentry'):
|
||||
form = forms.JournalEntryForm(
|
||||
@@ -381,11 +388,6 @@ class ObjectJournalView(View):
|
||||
# fall back to using base.html.
|
||||
if self.base_template is None:
|
||||
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
|
||||
# TODO: This can be removed once an object view has been established for every model.
|
||||
try:
|
||||
template.loader.get_template(self.base_template)
|
||||
except template.TemplateDoesNotExist:
|
||||
self.base_template = 'base.html'
|
||||
|
||||
return render(request, 'extras/object_journal.html', {
|
||||
'object': obj,
|
||||
|
||||
Reference in New Issue
Block a user