Merge branch 'develop' into develop-2.7

This commit is contained in:
Jeremy Stretch
2019-10-10 13:41:10 -04:00
86 changed files with 3001 additions and 2603 deletions

View File

@@ -86,6 +86,10 @@ class CustomLinkForm(forms.ModelForm):
class Meta:
model = CustomLink
exclude = []
widgets = {
'text': forms.Textarea,
'url': forms.Textarea,
}
help_texts = {
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
'which render as empty text will not be displayed.',

View File

@@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
from extras.constants import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer

View File

@@ -1,4 +1,3 @@
# Models which support custom fields
CUSTOMFIELD_MODELS = [
'circuits.circuit',

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag

View File

@@ -8,16 +8,12 @@ from taggit.forms import TagField
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.constants import COLOR_CHOICES
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
)
from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
OBJECTCHANGE_ACTION_CHOICES,
)
from .constants import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@@ -431,7 +427,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
help_text="Commit changes to the database (uncheck for a dry-run)"
)
def __init__(self, vars, *args, **kwargs):
def __init__(self, vars, *args, commit_default=True, **kwargs):
super().__init__(*args, **kwargs)
@@ -439,6 +435,10 @@ class ScriptForm(BootstrapMixin, forms.Form):
for name, var in vars.items():
self.fields[name] = var.as_field()
# Toggle default commit behavior based on Meta option
if not commit_default:
self.fields['_commit'].initial = False
# Move _commit to the end of the form
self.fields.move_to_end('_commit', True)

View File

@@ -9,9 +9,7 @@ from django.utils import timezone
from django.utils.functional import curry
from django_prometheus.models import model_deletes, model_inserts, model_updates
from .constants import (
OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE,
)
from .constants import *
from .models import ObjectChange
from .signals import purge_changelog
from .webhooks import enqueue_webhooks

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from datetime import date
import graphviz
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -654,7 +655,10 @@ class ConfigContext(models.Model):
class ConfigContextModel(models.Model):
"""
A model which includes local configuration context data. This local data will override any inherited data from
ConfigContexts.
"""
local_context_data = JSONField(
blank=True,
null=True,
@@ -679,6 +683,16 @@ class ConfigContextModel(models.Model):
return data
def clean(self):
super().clean()
# Verify that JSON data is provided as an object
if self.local_context_data and type(self.local_context_data) is not dict:
raise ValidationError(
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
)
#
# Custom scripts

View File

@@ -6,7 +6,7 @@ from collections import OrderedDict
from django.conf import settings
from django.utils import timezone
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
from .constants import *
from .models import ReportResult

View File

@@ -1,17 +1,17 @@
from collections import OrderedDict
import inspect
import json
import os
import pkgutil
import time
import traceback
import yaml
from collections import OrderedDict
import yaml
from django import forms
from django.conf import settings
from django.core.validators import RegexValidator
from django.db import transaction
from mptt.forms import TreeNodeChoiceField
from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
from mptt.models import MPTTModel
from ipam.formfields import IPFormField
@@ -21,13 +21,13 @@ from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARN
from .forms import ScriptForm
from .signals import purge_changelog
__all__ = [
'BaseScript',
'BooleanVar',
'FileVar',
'IntegerVar',
'IPNetworkVar',
'MultiObjectVar',
'ObjectVar',
'Script',
'StringVar',
@@ -150,6 +150,23 @@ class ObjectVar(ScriptVariable):
self.form_field = TreeNodeChoiceField
class MultiObjectVar(ScriptVariable):
"""
Like ObjectVar, but can represent one or more objects.
"""
form_field = forms.ModelMultipleChoiceField
def __init__(self, queryset, *args, **kwargs):
super().__init__(*args, **kwargs)
# Queryset for field choices
self.field_attrs['queryset'] = queryset
# Update form field for MPTT (nested) objects
if issubclass(queryset.model, MPTTModel):
self.form_field = TreeNodeMultipleChoiceField
class FileVar(ScriptVariable):
"""
An uploaded file.
@@ -226,7 +243,7 @@ class BaseScript:
Return a Django form suitable for populating the context data required to run this Script.
"""
vars = self._get_vars()
form = ScriptForm(vars, data, files)
form = ScriptForm(vars, data, files, commit_default=getattr(self.Meta, 'commit_default', True))
return form

View File

@@ -120,6 +120,29 @@ class ScriptVariablesTest(TestCase):
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['var1'].pk, data['var1'])
def test_multiobjectvar(self):
class TestScript(Script):
var1 = MultiObjectVar(
queryset=DeviceRole.objects.all()
)
# Populate some objects
for i in range(1, 6):
DeviceRole(
name='Device Role {}'.format(i),
slug='device-role-{}'.format(i)
).save()
# Validate valid data
data = {'var1': [role.pk for role in DeviceRole.objects.all()[:3]]}
form = TestScript().as_form(data, None)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['var1'][0].pk, data['var1'][0])
self.assertEqual(form.cleaned_data['var1'][1].pk, data['var1'][1])
self.assertEqual(form.cleaned_data['var1'][2].pk, data['var1'][2])
def test_filevar(self):
class TestScript(Script):

View File

@@ -3,10 +3,9 @@ import datetime
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
from extras.models import Webhook
from utilities.api import get_serializer_for_model
from .constants import WEBHOOK_MODELS
from .constants import *
def enqueue_webhooks(instance, user, request_id, action):

View File

@@ -6,7 +6,7 @@ import requests
from django_rq import job
from rest_framework.utils.encoders import JSONEncoder
from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJECTCHANGE_ACTION_CHOICES
from .constants import *
@job('default')