mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 01:06:11 -06:00
12552 initial removal of mptt
This commit is contained in:
parent
4208b79514
commit
da0c459e73
@ -26,9 +26,9 @@ django-filter
|
||||
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
|
||||
django-graphiql-debug-toolbar
|
||||
|
||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||
django-mptt
|
||||
# Adjacency-list trees for Django using recursive common table expressions.
|
||||
# https://github.com/matthiask/django-tree-queries/blob/main/CHANGELOG.rst
|
||||
django-tree-queries
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
|
@ -60,7 +60,7 @@ __all__ = [
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
@ -73,7 +73,7 @@ class NestedRegionSerializer(WritableNestedSerializer):
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
@ -98,7 +98,7 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
@ -258,7 +258,7 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemTemplate
|
||||
@ -433,7 +433,7 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
|
@ -589,7 +589,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
@ -1023,7 +1023,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
|
@ -99,13 +99,14 @@ class PassThroughPortMixin(object):
|
||||
#
|
||||
|
||||
class RegionViewSet(NetBoxModelViewSet):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
queryset = Region.objects.all().prefetch_related('tags')
|
||||
# queryset = Region.objects.add_related_count(
|
||||
# Region.objects.all(),
|
||||
# Site,
|
||||
# 'region',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('tags')
|
||||
serializer_class = serializers.RegionSerializer
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
@ -115,13 +116,14 @@ class RegionViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class SiteGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
queryset = SiteGroup.objects.all().prefetch_related('tags')
|
||||
# queryset = SiteGroup.objects.add_related_count(
|
||||
# SiteGroup.objects.all(),
|
||||
# Site,
|
||||
# 'group',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('tags')
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
@ -150,19 +152,20 @@ class SiteViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class LocationViewSet(NetBoxModelViewSet):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site', 'tags')
|
||||
queryset = Location.objects.all().prefetch_related('site', 'tags')
|
||||
# queryset = Location.objects.add_related_count(
|
||||
# Location.objects.add_related_count(
|
||||
# Location.objects.all(),
|
||||
# Device,
|
||||
# 'location',
|
||||
# 'device_count',
|
||||
# cumulative=True
|
||||
# ),
|
||||
# Rack,
|
||||
# 'location',
|
||||
# 'rack_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('site', 'tags')
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
|
@ -0,0 +1,148 @@
|
||||
# Generated by Django 4.1.8 on 2023-05-18 20:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0173_remove_napalm_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitem',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitem',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitem',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitem',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitemtemplate',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitemtemplate',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitemtemplate',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryitemtemplate',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='location',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='location',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='location',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='location',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='region',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='region',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='region',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='region',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sitegroup',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sitegroup',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sitegroup',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sitegroup',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitem',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.inventoryitem',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryitemtemplate',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.inventoryitemtemplate',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.location',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='region',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.region',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sitegroup',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='dcim.sitegroup',
|
||||
),
|
||||
),
|
||||
]
|
@ -4,14 +4,14 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from tree_queries.models import TreeNode
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.tree_queries import TreeManager
|
||||
from .device_components import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||
RearPort,
|
||||
@ -618,18 +618,10 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
}
|
||||
|
||||
|
||||
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||
class InventoryItemTemplate(TreeNode, ComponentTemplateModel):
|
||||
"""
|
||||
A template for an InventoryItem to be created for a new parent Device.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='child_items',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
component_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
|
||||
|
@ -8,7 +8,7 @@ from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from tree_queries.models import TreeNode
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
@ -16,9 +16,9 @@ from dcim.fields import MACAddressField, WWNField
|
||||
from netbox.models import OrganizationalModel, NetBoxModel
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.tree_queries import TreeManager
|
||||
from wireless.choices import *
|
||||
from wireless.utils import get_channel_attr
|
||||
|
||||
@ -1064,19 +1064,11 @@ class InventoryItemRole(OrganizationalModel):
|
||||
return reverse('dcim:inventoryitemrole', args=[self.pk])
|
||||
|
||||
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
class InventoryItem(TreeNode, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
InventoryItems are used only for inventory purposes.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='child_items',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
component_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||
|
@ -198,13 +198,14 @@ class PathTraceView(generic.ObjectView):
|
||||
#
|
||||
|
||||
class RegionListView(generic.ObjectListView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = Region.objects.all()
|
||||
# queryset = Region.objects.add_related_count(
|
||||
# Region.objects.all(),
|
||||
# Site,
|
||||
# 'region',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.RegionFilterSet
|
||||
filterset_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
@ -244,26 +245,28 @@ class RegionBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RegionBulkEditView(generic.BulkEditView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = Region.objects.all()
|
||||
# queryset = Region.objects.add_related_count(
|
||||
# Region.objects.all(),
|
||||
# Site,
|
||||
# 'region',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
form = forms.RegionBulkEditForm
|
||||
|
||||
|
||||
class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Region.objects.add_related_count(
|
||||
Region.objects.all(),
|
||||
Site,
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = Region.objects.all()
|
||||
# queryset = Region.objects.add_related_count(
|
||||
# Region.objects.all(),
|
||||
# Site,
|
||||
# 'region',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
|
||||
@ -278,13 +281,14 @@ class RegionContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class SiteGroupListView(generic.ObjectListView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = SiteGroup.objects.all()
|
||||
# queryset = SiteGroup.objects.add_related_count(
|
||||
# SiteGroup.objects.all(),
|
||||
# Site,
|
||||
# 'group',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
filterset_form = forms.SiteGroupFilterForm
|
||||
table = tables.SiteGroupTable
|
||||
@ -324,26 +328,28 @@ class SiteGroupBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = SiteGroup.objects.all()
|
||||
# queryset = SiteGroup.objects.add_related_count(
|
||||
# SiteGroup.objects.all(),
|
||||
# Site,
|
||||
# 'group',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
form = forms.SiteGroupBulkEditForm
|
||||
|
||||
|
||||
class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SiteGroup.objects.add_related_count(
|
||||
SiteGroup.objects.all(),
|
||||
Site,
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = SiteGroup.objects.all()
|
||||
# queryset = SiteGroup.objects.add_related_count(
|
||||
# SiteGroup.objects.all(),
|
||||
# Site,
|
||||
# 'group',
|
||||
# 'site_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
@ -388,20 +394,21 @@ class SiteView(generic.ObjectView):
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
||||
)
|
||||
|
||||
locations = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
locations = Location.objects.add_related_count(
|
||||
locations,
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=instance)
|
||||
locations = Location.objects.all().restrict(request.user, 'view').filter(site=instance)
|
||||
# locations = Location.objects.add_related_count(
|
||||
# Location.objects.all(),
|
||||
# Rack,
|
||||
# 'location',
|
||||
# 'rack_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
# locations = Location.objects.add_related_count(
|
||||
# locations,
|
||||
# Device,
|
||||
# 'location',
|
||||
# 'device_count',
|
||||
# cumulative=True
|
||||
# ).restrict(request.user, 'view').filter(site=instance)
|
||||
|
||||
nonracked_devices = Device.objects.filter(
|
||||
site=instance,
|
||||
@ -456,19 +463,20 @@ class SiteContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class LocationListView(generic.ObjectListView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = Location.objects.all()
|
||||
# queryset = Location.objects.add_related_count(
|
||||
# Location.objects.add_related_count(
|
||||
# Location.objects.all(),
|
||||
# Device,
|
||||
# 'location',
|
||||
# 'device_count',
|
||||
# cumulative=True
|
||||
# ),
|
||||
# Rack,
|
||||
# 'location',
|
||||
# 'rack_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.LocationFilterSet
|
||||
filterset_form = forms.LocationFilterForm
|
||||
table = tables.LocationTable
|
||||
@ -515,26 +523,28 @@ class LocationBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class LocationBulkEditView(generic.BulkEditView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
queryset = Location.objects.all()
|
||||
# queryset = Location.objects.add_related_count(
|
||||
# Location.objects.all(),
|
||||
# Rack,
|
||||
# 'location',
|
||||
# 'rack_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('site')
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
form = forms.LocationBulkEditForm
|
||||
|
||||
|
||||
class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
queryset = Location.objects.all()
|
||||
# queryset = Location.objects.add_related_count(
|
||||
# Location.objects.all(),
|
||||
# Rack,
|
||||
# 'location',
|
||||
# 'rack_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('site')
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
|
||||
|
@ -130,10 +130,11 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
region_field = 'cluster__site__region'
|
||||
sitegroup_field = 'cluster__site__group'
|
||||
|
||||
"""
|
||||
base_query.add(
|
||||
(Q(
|
||||
regions__tree_id=OuterRef(f'{region_field}__tree_id'),
|
||||
regions__level__lte=OuterRef(f'{region_field}__level'),
|
||||
regions__tree_depth__lte=OuterRef(f'{region_field}__tree_depth'),
|
||||
regions__lft__lte=OuterRef(f'{region_field}__lft'),
|
||||
regions__rght__gte=OuterRef(f'{region_field}__rght'),
|
||||
) | Q(regions=None)),
|
||||
@ -143,11 +144,12 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
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__tree_depth__lte=OuterRef(f'{sitegroup_field}__tree_depth'),
|
||||
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
|
||||
|
@ -21,7 +21,7 @@ class NestedGroupModelSerializer(NetBoxModelSerializer):
|
||||
"""
|
||||
Extends PrimaryModelSerializer to include MPTT support.
|
||||
"""
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
|
||||
class BulkOperationSerializer(serializers.Serializer):
|
||||
|
@ -2,11 +2,11 @@ from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from tree_queries.models import TreeNode
|
||||
|
||||
from netbox.models.features import *
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.tree_queries import TreeManager
|
||||
|
||||
__all__ = (
|
||||
'ChangeLoggedModel',
|
||||
@ -103,19 +103,11 @@ class PrimaryModel(NetBoxModel):
|
||||
abstract = True
|
||||
|
||||
|
||||
class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel):
|
||||
class NestedGroupModel(CloningMixin, NetBoxFeatureSet, TreeNode):
|
||||
"""
|
||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
@ -131,9 +123,7 @@ class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('name',)
|
||||
ordering = ("name",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -24,7 +24,7 @@ __all__ = [
|
||||
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||
tenant_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
@ -49,7 +49,7 @@ class NestedTenantSerializer(WritableNestedSerializer):
|
||||
class NestedContactGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||
contact_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
|
@ -24,13 +24,14 @@ class TenancyRootView(APIRootView):
|
||||
#
|
||||
|
||||
class TenantGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = TenantGroup.objects.add_related_count(
|
||||
TenantGroup.objects.all(),
|
||||
Tenant,
|
||||
'group',
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
queryset = TenantGroup.objects.all().prefetch_related('tags')
|
||||
# queryset = TenantGroup.objects.add_related_count(
|
||||
# TenantGroup.objects.all(),
|
||||
# Tenant,
|
||||
# 'group',
|
||||
# 'tenant_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('tags')
|
||||
serializer_class = serializers.TenantGroupSerializer
|
||||
filterset_class = filtersets.TenantGroupFilterSet
|
||||
|
||||
@ -59,13 +60,14 @@ class TenantViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ContactGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
queryset = ContactGroup.objects.all().prefetch_related('tags')
|
||||
# queryset = ContactGroup.objects.add_related_count(
|
||||
# ContactGroup.objects.all(),
|
||||
# Contact,
|
||||
# 'group',
|
||||
# 'contact_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('tags')
|
||||
serializer_class = serializers.ContactGroupSerializer
|
||||
filterset_class = filtersets.ContactGroupFilterSet
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
# Generated by Django 4.1.8 on 2023-05-18 20:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('tenancy', '0010_tenant_relax_uniqueness'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='contactgroup',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contactgroup',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contactgroup',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contactgroup',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tenantgroup',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tenantgroup',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tenantgroup',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tenantgroup',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contactgroup',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='tenancy.contactgroup',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenantgroup',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='tenancy.tenantgroup',
|
||||
),
|
||||
),
|
||||
]
|
@ -42,13 +42,14 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
||||
|
||||
|
||||
class TenantGroupListView(generic.ObjectListView):
|
||||
queryset = TenantGroup.objects.add_related_count(
|
||||
TenantGroup.objects.all(),
|
||||
Tenant,
|
||||
'group',
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = TenantGroup.objects.all()
|
||||
# queryset = TenantGroup.objects.add_related_count(
|
||||
# TenantGroup.objects.all(),
|
||||
# Tenant,
|
||||
# 'group',
|
||||
# 'tenant_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.TenantGroupFilterSet
|
||||
filterset_form = forms.TenantGroupFilterForm
|
||||
table = tables.TenantGroupTable
|
||||
@ -86,26 +87,28 @@ class TenantGroupBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class TenantGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = TenantGroup.objects.add_related_count(
|
||||
TenantGroup.objects.all(),
|
||||
Tenant,
|
||||
'group',
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = TenantGroup.objects.all()
|
||||
# queryset = TenantGroup.objects.add_related_count(
|
||||
# TenantGroup.objects.all(),
|
||||
# Tenant,
|
||||
# 'group',
|
||||
# 'tenant_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.TenantGroupFilterSet
|
||||
table = tables.TenantGroupTable
|
||||
form = forms.TenantGroupBulkEditForm
|
||||
|
||||
|
||||
class TenantGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = TenantGroup.objects.add_related_count(
|
||||
TenantGroup.objects.all(),
|
||||
Tenant,
|
||||
'group',
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = TenantGroup.objects.all()
|
||||
# queryset = TenantGroup.objects.add_related_count(
|
||||
# TenantGroup.objects.all(),
|
||||
# Tenant,
|
||||
# 'group',
|
||||
# 'tenant_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.TenantGroupFilterSet
|
||||
table = tables.TenantGroupTable
|
||||
|
||||
@ -198,13 +201,14 @@ class TenantContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class ContactGroupListView(generic.ObjectListView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = ContactGroup.objects.all()
|
||||
# queryset = ContactGroup.objects.add_related_count(
|
||||
# ContactGroup.objects.all(),
|
||||
# Contact,
|
||||
# 'group',
|
||||
# 'contact_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.ContactGroupFilterSet
|
||||
filterset_form = forms.ContactGroupFilterForm
|
||||
table = tables.ContactGroupTable
|
||||
@ -242,26 +246,28 @@ class ContactGroupBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class ContactGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = ContactGroup.objects.all()
|
||||
# queryset = ContactGroup.objects.add_related_count(
|
||||
# ContactGroup.objects.all(),
|
||||
# Contact,
|
||||
# 'group',
|
||||
# 'contact_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.ContactGroupFilterSet
|
||||
table = tables.ContactGroupTable
|
||||
form = forms.ContactGroupBulkEditForm
|
||||
|
||||
|
||||
class ContactGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ContactGroup.objects.add_related_count(
|
||||
ContactGroup.objects.all(),
|
||||
Contact,
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = ContactGroup.objects.all()
|
||||
# queryset = ContactGroup.objects.add_related_count(
|
||||
# ContactGroup.objects.all(),
|
||||
# Contact,
|
||||
# 'group',
|
||||
# 'contact_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.ContactGroupFilterSet
|
||||
table = tables.ContactGroupTable
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
from mptt.managers import TreeManager as TreeManager_
|
||||
from mptt.querysets import TreeQuerySet as TreeQuerySet_
|
||||
|
||||
from django.db.models import Manager
|
||||
from .querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'TreeManager',
|
||||
'TreeQuerySet',
|
||||
)
|
||||
|
||||
|
||||
class TreeQuerySet(TreeQuerySet_, RestrictedQuerySet):
|
||||
"""
|
||||
Mate django-mptt's TreeQuerySet with our RestrictedQuerySet for permissions enforcement.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TreeManager(Manager.from_queryset(TreeQuerySet), TreeManager_):
|
||||
"""
|
||||
Extend django-mptt's TreeManager to incorporate RestrictedQuerySet().
|
||||
"""
|
||||
pass
|
25
netbox/utilities/tree_queries.py
Normal file
25
netbox/utilities/tree_queries.py
Normal file
@ -0,0 +1,25 @@
|
||||
from tree_queries.query import TreeManager as TreeManager_
|
||||
from tree_queries.query import TreeQuerySet as TreeQuerySet_
|
||||
|
||||
from django.db.models import Manager
|
||||
from .querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'TreeManager',
|
||||
'TreeQuerySet',
|
||||
)
|
||||
|
||||
|
||||
class TreeQuerySet(TreeQuerySet_, RestrictedQuerySet):
|
||||
"""
|
||||
Mate django-tree-queries TreeQuerySet with our RestrictedQuerySet for permissions enforcement.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TreeManager(Manager.from_queryset(TreeQuerySet), TreeManager_):
|
||||
"""
|
||||
Extend django-tree-queries TreeManager to incorporate RestrictedQuerySet().
|
||||
"""
|
||||
|
||||
_with_tree_fields = True
|
@ -15,7 +15,7 @@ from django.utils.html import escape
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localtime
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from mptt.models import MPTTModel
|
||||
from tree_queries.models import TreeNode
|
||||
|
||||
from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
|
||||
from extras.plugins import PluginConfig
|
||||
@ -153,8 +153,8 @@ def serialize_object(obj, resolve_tags=True, extra=None):
|
||||
json_str = serializers.serialize('json', [obj])
|
||||
data = json.loads(json_str)[0]['fields']
|
||||
|
||||
# Exclude any MPTTModel fields
|
||||
if issubclass(obj.__class__, MPTTModel):
|
||||
# Exclude any TreeNode fields
|
||||
if issubclass(obj.__class__, TreeNode):
|
||||
for field in ['level', 'lft', 'rght', 'tree_id']:
|
||||
data.pop(field)
|
||||
|
||||
|
@ -17,7 +17,7 @@ __all__ = (
|
||||
class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||
wirelesslan_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
_depth = serializers.IntegerField(source='tree_depth', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WirelessLANGroup
|
||||
|
@ -15,13 +15,14 @@ class WirelessRootView(APIRootView):
|
||||
|
||||
|
||||
class WirelessLANGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = WirelessLANGroup.objects.add_related_count(
|
||||
WirelessLANGroup.objects.all(),
|
||||
WirelessLAN,
|
||||
'group',
|
||||
'wirelesslan_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = WirelessLANGroup.objects.all()
|
||||
# queryset = WirelessLANGroup.objects.add_related_count(
|
||||
# WirelessLANGroup.objects.all(),
|
||||
# WirelessLAN,
|
||||
# 'group',
|
||||
# 'wirelesslan_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
serializer_class = serializers.WirelessLANGroupSerializer
|
||||
filterset_class = filtersets.WirelessLANGroupFilterSet
|
||||
|
||||
|
@ -0,0 +1,40 @@
|
||||
# Generated by Django 4.1.8 on 2023-05-18 20:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('wireless', '0008_wirelesslan_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='wirelesslangroup',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='wirelesslangroup',
|
||||
name='lft',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='wirelesslangroup',
|
||||
name='rght',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='wirelesslangroup',
|
||||
name='tree_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='wirelesslangroup',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='children',
|
||||
to='wireless.wirelesslangroup',
|
||||
),
|
||||
),
|
||||
]
|
@ -1,7 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from dcim.choices import LinkStatusChoices
|
||||
from dcim.constants import WIRELESS_IFACE_TYPES
|
||||
|
@ -11,13 +11,14 @@ from .models import *
|
||||
#
|
||||
|
||||
class WirelessLANGroupListView(generic.ObjectListView):
|
||||
queryset = WirelessLANGroup.objects.add_related_count(
|
||||
WirelessLANGroup.objects.all(),
|
||||
WirelessLAN,
|
||||
'group',
|
||||
'wirelesslan_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
queryset = WirelessLANGroup.objects.all().prefetch_related('tags')
|
||||
# queryset = WirelessLANGroup.objects.add_related_count(
|
||||
# WirelessLANGroup.objects.all(),
|
||||
# WirelessLAN,
|
||||
# 'group',
|
||||
# 'wirelesslan_count',
|
||||
# cumulative=True
|
||||
# ).prefetch_related('tags')
|
||||
filterset = filtersets.WirelessLANGroupFilterSet
|
||||
filterset_form = forms.WirelessLANGroupFilterForm
|
||||
table = tables.WirelessLANGroupTable
|
||||
@ -55,26 +56,28 @@ class WirelessLANGroupBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class WirelessLANGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = WirelessLANGroup.objects.add_related_count(
|
||||
WirelessLANGroup.objects.all(),
|
||||
WirelessLAN,
|
||||
'group',
|
||||
'wirelesslan_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = WirelessLANGroup.objects.all()
|
||||
# queryset = WirelessLANGroup.objects.add_related_count(
|
||||
# WirelessLANGroup.objects.all(),
|
||||
# WirelessLAN,
|
||||
# 'group',
|
||||
# 'wirelesslan_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.WirelessLANGroupFilterSet
|
||||
table = tables.WirelessLANGroupTable
|
||||
form = forms.WirelessLANGroupBulkEditForm
|
||||
|
||||
|
||||
class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = WirelessLANGroup.objects.add_related_count(
|
||||
WirelessLANGroup.objects.all(),
|
||||
WirelessLAN,
|
||||
'group',
|
||||
'wirelesslan_count',
|
||||
cumulative=True
|
||||
)
|
||||
queryset = WirelessLANGroup.objects.all()
|
||||
# queryset = WirelessLANGroup.objects.add_related_count(
|
||||
# WirelessLANGroup.objects.all(),
|
||||
# WirelessLAN,
|
||||
# 'group',
|
||||
# 'wirelesslan_count',
|
||||
# cumulative=True
|
||||
# )
|
||||
filterset = filtersets.WirelessLANGroupFilterSet
|
||||
table = tables.WirelessLANGroupTable
|
||||
|
||||
|
@ -5,7 +5,6 @@ django-cors-headers==3.14.0
|
||||
django-debug-toolbar==4.0.0
|
||||
django-filter==23.2
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-mptt==0.14
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.3.1
|
||||
django-redis==5.2.0
|
||||
@ -14,6 +13,7 @@ django-rq==2.8.0
|
||||
django-tables2==2.5.3
|
||||
django-taggit==4.0.0
|
||||
django-timezone-field==5.0
|
||||
django-tree-queries==0.14.0
|
||||
djangorestframework==3.14.0
|
||||
drf-spectacular==0.26.2
|
||||
drf-spectacular-sidecar==2023.5.1
|
||||
|
Loading…
Reference in New Issue
Block a user