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

Merge branch 'develop' into 738-detect-new-releases

This commit is contained in:
Jeremy Stretch
2020-03-13 10:05:24 -04:00
168 changed files with 1689 additions and 2569 deletions

View File

@@ -234,6 +234,7 @@ class ValidatedModelSerializer(ModelSerializer):
for k, v in attrs.items():
setattr(instance, k, v)
instance.clean()
instance.validate_unique()
return data

View File

@@ -76,26 +76,28 @@ class CustomChoiceFieldInspector(FieldInspector):
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, ChoiceField):
value_schema = openapi.Schema(type=openapi.TYPE_STRING)
choices = field._choices
choice_value = list(choices.keys())
choice_label = list(choices.values())
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
choices = list(field._choices.keys())
if set([None] + choices) == {None, True, False}:
if set([None] + choice_value) == {None, True, False}:
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
# differentiated since they each have subtly different values in their choice keys.
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
# - face is an integer set {0, 1} which is easily confused with {False, True}
schema_type = openapi.TYPE_STRING
if all(type(x) == bool for x in [c for c in choices if c is not None]):
if all(type(x) == bool for x in [c for c in choice_value if c is not None]):
schema_type = openapi.TYPE_BOOLEAN
value_schema = openapi.Schema(type=schema_type)
value_schema = openapi.Schema(type=schema_type, enum=choice_value)
value_schema['x-nullable'] = True
if isinstance(choices[0], int):
if isinstance(choice_value[0], int):
# Change value_schema for IPAddressFamilyChoices, RackWidthChoices
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER, enum=choice_value)
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
"label": openapi.Schema(type=openapi.TYPE_STRING),
"label": openapi.Schema(type=openapi.TYPE_STRING, enum=choice_label),
"value": value_schema
})

View File

@@ -498,14 +498,14 @@ class ExpandableIPAddressField(forms.CharField):
class CommentField(forms.CharField):
"""
A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text.
"""
widget = forms.Textarea
default_label = ''
# TODO: Port GFM syntax cheat sheet to internal documentation
# TODO: Port Markdown cheat sheet to internal documentation
default_helptext = '<i class="fa fa-info-circle"></i> '\
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
'GitHub-Flavored Markdown</a> syntax is supported'
'Markdown</a> syntax is supported'
def __init__(self, *args, **kwargs):
required = kwargs.pop('required', False)

View File

@@ -4,6 +4,7 @@ import re
import yaml
from django import template
from django.conf import settings
from django.urls import NoReverseMatch, reverse
from django.utils.html import strip_tags
from django.utils.safestring import mark_safe
@@ -19,15 +20,6 @@ register = template.Library()
# Filters
#
@register.filter()
def oneline(value):
"""
Replace each line break with a single space
"""
value = value.replace('\r', '')
return value.replace('\n', ' ')
@register.filter()
def placeholder(value):
"""
@@ -39,32 +31,16 @@ def placeholder(value):
return mark_safe(placeholder)
@register.filter()
def getlist(value, arg):
"""
Return all values of a QueryDict key
"""
return value.getlist(arg)
@register.filter
def getkey(value, key):
"""
Return a dictionary item specified by key
"""
return value[key]
@register.filter(is_safe=True)
def gfm(value):
def render_markdown(value):
"""
Render text as GitHub-Flavored Markdown
Render text as Markdown
"""
# Strip HTML tags
value = strip_tags(value)
# Render Markdown with GFM extension
html = markdown(value, extensions=['mdx_gfm'])
# Render Markdown
html = markdown(value, extensions=['fenced_code', 'tables'])
return mark_safe(html)
@@ -86,19 +62,12 @@ def render_yaml(value):
@register.filter()
def model_name(obj):
def meta(obj, attr):
"""
Return the name of the model of the given object
Return the specified Meta attribute of a model. This is needed because Django does not permit templates
to access attributes which begin with an underscore (e.g. _meta).
"""
return obj._meta.verbose_name
@register.filter()
def model_name_plural(obj):
"""
Return the plural name of the model of the given object
"""
return obj._meta.verbose_name_plural
return getattr(obj._meta, attr, '')
@register.filter()
@@ -116,14 +85,6 @@ def url_name(model, action):
return None
@register.filter()
def contains(value, arg):
"""
Test whether a value contains any of a given set of strings. `arg` should be a comma-separated list of strings.
"""
return any(s in value for s in arg.split(','))
@register.filter()
def bettertitle(value):
"""
@@ -216,6 +177,30 @@ def percentage(x, y):
return round(x / y * 100)
@register.filter()
def get_docs(model):
"""
Render and return documentation for the specified model.
"""
path = '{}/models/{}/{}.md'.format(
settings.DOCS_ROOT,
model._meta.app_label,
model._meta.model_name
)
try:
with open(path) as docfile:
content = docfile.read()
except FileNotFoundError:
return "Unable to load documentation, file not found: {}".format(path)
except IOError:
return "Unable to load documentation, error reading file: {}".format(path)
# Render Markdown with the admonition extension
content = markdown(content, extensions=['admonition', 'fenced_code', 'tables'])
return mark_safe(content)
#
# Tags
#

View File

@@ -33,14 +33,20 @@ class NaturalizationTestCase(TestCase):
# IOS/JunOS-style
('Gi', '9999999999999999Gi000000000000000000'),
('Gi1', '9999999999999999Gi000001000000000000'),
('Gi1.0', '9999999999999999Gi000001000000000000'),
('Gi1.1', '9999999999999999Gi000001000000000001'),
('Gi1:0', '9999999999999999Gi000001000000000000'),
('Gi1:0.0', '9999999999999999Gi000001000000000000'),
('Gi1:0.1', '9999999999999999Gi000001000000000001'),
('Gi1:1', '9999999999999999Gi000001000001000000'),
('Gi1:1.0', '9999999999999999Gi000001000001000000'),
('Gi1:1.1', '9999999999999999Gi000001000001000001'),
('Gi1/2', '0001999999999999Gi000002000000000000'),
('Gi1/2/3', '0001000299999999Gi000003000000000000'),
('Gi1/2/3/4', '0001000200039999Gi000004000000000000'),
('Gi1/2/3/4/5', '0001000200030004Gi000005000000000000'),
('Gi1/2/3/4/5:6', '0001000200030004Gi000005000006000000'),
('Gi1/2/3/4/5:6.7', '0001000200030004Gi000005000006000007'),
('Gi1:2', '9999999999999999Gi000001000002000000'),
('Gi1:2.3', '9999999999999999Gi000001000002000003'),
# Generic
('Interface 1', '9999999999999999Interface 000001000000000000'),
('Interface 1 (other)', '9999999999999999Interface 000001000000000000 (other)'),

View File

@@ -544,7 +544,7 @@ class BulkImportView(GetReturnURLMixin, View):
return ImportForm(*args, **kwargs)
def _save_obj(self, obj_form):
def _save_obj(self, obj_form, request):
"""
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
"""
@@ -573,7 +573,7 @@ class BulkImportView(GetReturnURLMixin, View):
for row, data in enumerate(form.cleaned_data['csv'], start=1):
obj_form = self.model_form(data)
if obj_form.is_valid():
obj = self._save_obj(obj_form)
obj = self._save_obj(obj_form, request)
new_objs.append(obj)
else:
for field, err in obj_form.errors.items():