Merge branch 'develop' into develop-2.9

This commit is contained in:
Jeremy Stretch
2020-06-15 16:04:32 -04:00
27 changed files with 384 additions and 157 deletions

View File

@@ -7,6 +7,7 @@ import django_filters
import yaml
from django import forms
from django.conf import settings
from django.contrib.postgres.forms import SimpleArrayField
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count
@@ -243,24 +244,11 @@ class ContentTypeSelect(StaticSelect2):
option_template_name = 'widgets/select_contenttype.html'
class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
"""
MultiSelect widget for a SimpleArrayField. Choices must be populated on the widget.
"""
def __init__(self, *args, **kwargs):
self.delimiter = kwargs.pop('delimiter', ',')
super().__init__(*args, **kwargs)
class NumericArrayField(SimpleArrayField):
def optgroups(self, name, value, attrs=None):
# Split the delimited string of values into a list
if value:
value = value[0].split(self.delimiter)
return super().optgroups(name, value, attrs)
def value_from_datadict(self, data, files, name):
# Condense the list of selected choices into a delimited string
data = super().value_from_datadict(data, files, name)
return self.delimiter.join(data)
def to_python(self, value):
value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value)
class APISelect(SelectWithDisabled):
@@ -661,9 +649,8 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
class LaxURLField(forms.URLField):
"""
Modifies Django's built-in URLField in two ways:
1) Allow any valid scheme per RFC 3986 section 3.1
2) Remove the requirement for fully-qualified domain names (e.g. http://myserver/ is valid)
Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
(e.g. http://myserver/ is valid)
"""
default_validators = [EnhancedURLValidator()]

View File

@@ -84,6 +84,10 @@ class BaseTable(tables.Table):
return [name for name in self.sequence if self.columns[name].visible]
#
# Table columns
#
class ToggleColumn(tables.CheckBoxColumn):
"""
Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
@@ -129,6 +133,19 @@ class ColorColumn(tables.Column):
)
class ColoredLabelColumn(tables.TemplateColumn):
"""
Render a colored label (e.g. for DeviceRoles).
"""
template_code = """
{% load helpers %}
{% if value %}<label class="label" style="color: {{ value.color|fgcolor }}; background-color: #{{ value.color }}">{{ value }}</label>{% else %}&mdash;{% endif %}
"""
def __init__(self, *args, **kwargs):
super().__init__(template_code=self.template_code, *args, **kwargs)
class TagColumn(tables.TemplateColumn):
"""
Display a list of tags assigned to the object.

View File

@@ -10,7 +10,6 @@ from django.utils.html import strip_tags
from django.utils.safestring import mark_safe
from markdown import markdown
from utilities.choices import unpack_grouped_choices
from utilities.utils import foreground_color
register = template.Library()
@@ -39,6 +38,11 @@ def render_markdown(value):
# Strip HTML tags
value = strip_tags(value)
# Sanitize Markdown links
schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)'
value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
# Render Markdown
html = markdown(value, extensions=['fenced_code', 'tables'])

View File

@@ -1,5 +1,6 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import ForeignKey, ManyToManyField
from django.forms.models import model_to_dict
@@ -64,7 +65,6 @@ class TestCase(_TestCase):
"""
Compare a model instance to a dictionary, checking that its attribute values match those specified
in the dictionary.
:instance: Python object instance
:data: Dictionary of test data used to define the instance
:api: Set to True is the data is a JSON representation of the instance
@@ -95,6 +95,12 @@ class TestCase(_TestCase):
elif type(value) is IPNetwork:
model_dict[key] = str(value)
else:
# Convert ArrayFields to CSV strings
if type(instance._meta.get_field(key)) is ArrayField:
model_dict[key] = ','.join([str(v) for v in value])
# Omit any dictionary keys which are not instance attributes
relevant_data = {
k: v for k, v in data.items() if hasattr(instance, k)

View File

@@ -1,31 +1,24 @@
import re
from django.conf import settings
from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
class EnhancedURLValidator(URLValidator):
"""
Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension.
Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed
schemes specified in the configuration.
"""
class AnyURLScheme(object):
"""
A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
"""
def __contains__(self, item):
if not item or not re.match(r'^[a-z][0-9a-z+\-.]*$', item.lower()):
return False
return True
fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
regex = _lazy_re_compile(
r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (previously enforced by AnyURLScheme or schemes kwarg)
r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (enforced separately)
r'(?:\S+(?::\S*)?@)?' # HTTP basic authentication
r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname
r'(?::\d{2,5})?' # Port number
r'(?:[/?#][^\s]*)?' # Path
r'\Z', re.IGNORECASE)
schemes = AnyURLScheme()
schemes = settings.ALLOWED_URL_SCHEMES
class ExclusionValidator(BaseValidator):

View File

@@ -944,6 +944,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
# TODO: Find a better way to accomplish this
if 'device' in request.GET:
initial_data['device'] = request.GET.get('device')
elif 'device_type' in request.GET:
initial_data['device_type'] = request.GET.get('device_type')
form = self.form(model, initial=initial_data)