1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

merge develop

This commit is contained in:
John Anderson
2020-02-13 16:00:07 -05:00
58 changed files with 1620 additions and 1146 deletions

View File

@@ -1,14 +1,15 @@
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup
from .choices import *
@@ -190,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
#
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
widget=StaticSelect2Multiple()
)
sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/sites/"
)
)
roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/"
)
)
platforms = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/platforms/"
)
)
cluster_groups = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/"
)
)
clusters = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/"
)
)
tenant_groups = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
)
)
tenants = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
@@ -204,36 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConfigContext
fields = [
fields = (
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
]
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'cluster_groups': APISelectMultiple(
api_url="/api/virtualization/cluster-groups/"
),
'clusters': APISelectMultiple(
api_url="/api/virtualization/clusters/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
),
}
)
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -265,72 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
region = FilterChoiceField(
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
)
site = FilterChoiceField(
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
role = FilterChoiceField(
role = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug",
)
)
platform = FilterChoiceField(
platform = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug",
)
)
cluster_group = FilterChoiceField(
cluster_group = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/",
value_field="slug",
)
)
cluster_id = FilterChoiceField(
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label='Cluster',
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/",
)
)
tenant_group = FilterChoiceField(
tenant_group = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
)
)
tenant = FilterChoiceField(
tenant = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
)
)
tag = FilterChoiceField(
tag = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/extras/tags/",
value_field="slug",
@@ -387,11 +425,14 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
)
action = forms.ChoiceField(
choices=add_blank_choice(ObjectChangeActionChoices),
required=False
required=False,
widget=StaticSelect2()
)
# TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
user = forms.ModelChoiceField(
queryset=User.objects.order_by('username'),
required=False
required=False,
widget=StaticSelect2()
)
changed_object_type = forms.ModelChoiceField(
queryset=ContentType.objects.order_by('model'),

View File

@@ -0,0 +1,111 @@
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
from utilities.fields import NaturalOrderingField
class Command(BaseCommand):
help = "Recalculate natural ordering values for the specified models"
def add_arguments(self, parser):
parser.add_argument(
'args', metavar='app_label.ModelName', nargs='*',
help='One or more specific models (each prefixed with its app_label) to renaturalize',
)
def _get_models(self, names):
"""
Compile a list of models to be renaturalized. If no names are specified, all models which have one or more
NaturalOrderingFields will be included.
"""
models = []
if names:
# Collect all NaturalOrderingFields present on the specified models
for name in names:
try:
app_label, model_name = name.split('.')
except ValueError:
raise CommandError(
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
)
try:
app_config = apps.get_app_config(app_label)
except LookupError as e:
raise CommandError(str(e))
try:
model = app_config.get_model(model_name)
except LookupError:
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if not fields:
raise CommandError(
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
)
models.append(
(model, fields)
)
else:
# Find *all* models with NaturalOrderingFields
for app_config in apps.get_app_configs():
for model in app_config.models.values():
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if fields:
models.append(
(model, fields)
)
return models
def handle(self, *args, **options):
models = self._get_models(args)
if options['verbosity']:
self.stdout.write("Renaturalizing {} models.".format(len(models)))
for model, fields in models:
for field in fields:
target_field = field.target_field
naturalize = field.naturalize_function
count = 0
# Print the model and field name
if options['verbosity']:
self.stdout.write(
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
ending='\n' if options['verbosity'] >= 2 else ''
)
self.stdout.flush()
# Find all unique values for the field
queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
for value in queryset:
naturalized_value = naturalize(value)
if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
self.stdout.flush()
# Update each unique field value in bulk
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
if options['verbosity'] >= 2:
self.stdout.write(" ({})".format(changed))
count += changed
# Print the total count of alterations for the field
if options['verbosity'] >= 2:
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
count, model._meta.verbose_name_plural, queryset.count()
)))
elif options['verbosity']:
self.stdout.write(self.style.SUCCESS(str(count)))
if options['verbosity']:
self.stdout.write(self.style.SUCCESS("Done."))

View File

@@ -48,7 +48,7 @@ class ScriptVariable:
"""
form_field = forms.CharField
def __init__(self, label='', description='', default=None, required=True):
def __init__(self, label='', description='', default=None, required=True, widget=None):
# Initialize field attributes
if not hasattr(self, 'field_attrs'):
@@ -59,6 +59,8 @@ class ScriptVariable:
self.field_attrs['help_text'] = description
if default:
self.field_attrs['initial'] = default
if widget:
self.field_attrs['widget'] = widget
self.field_attrs['required'] = required
# Initialize the list of optional validators if none have already been defined
@@ -71,7 +73,10 @@ class ScriptVariable:
"""
form_field = self.form_field(**self.field_attrs)
if not isinstance(form_field.widget, forms.CheckboxInput):
form_field.widget.attrs['class'] = 'form-control'
if form_field.widget.attrs and 'class' in form_field.widget.attrs.keys():
form_field.widget.attrs['class'] += ' form-control'
else:
form_field.widget.attrs['class'] = 'form-control'
return form_field

View File

@@ -7,10 +7,10 @@ from django.urls import reverse
from dcim.models import Site
from extras.choices import ObjectChangeActionChoices
from extras.models import ConfigContext, ObjectChange, Tag
from utilities.testing import StandardTestCases, TestCase
from utilities.testing import ViewTestCases, TestCase
class TagTestCase(StandardTestCases.Views):
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Tag
# Disable inapplicable tests
@@ -38,7 +38,7 @@ class TagTestCase(StandardTestCases.Views):
}
class ConfigContextTestCase(StandardTestCases.Views):
class ConfigContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ConfigContext
# Disable inapplicable tests