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:
@@ -234,6 +234,7 @@ class ValidatedModelSerializer(ModelSerializer):
|
||||
for k, v in attrs.items():
|
||||
setattr(instance, k, v)
|
||||
instance.clean()
|
||||
instance.validate_unique()
|
||||
|
||||
return data
|
||||
|
||||
|
@@ -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
|
||||
})
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
#
|
||||
|
@@ -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)'),
|
||||
|
@@ -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():
|
||||
|
Reference in New Issue
Block a user