2018-02-02 13:32:16 -05:00
|
|
|
import datetime
|
2018-06-22 14:00:23 -04:00
|
|
|
import json
|
2019-10-04 12:08:48 -04:00
|
|
|
from collections import OrderedDict
|
2018-02-02 13:32:16 -05:00
|
|
|
|
2018-06-22 14:00:23 -04:00
|
|
|
from django.core.serializers import serialize
|
2019-04-19 16:09:22 -04:00
|
|
|
from django.db.models import Count, OuterRef, Subquery
|
2020-02-05 09:15:48 -05:00
|
|
|
from django.http import QueryDict
|
2019-12-31 14:00:55 -05:00
|
|
|
from jinja2 import Environment
|
2018-02-02 13:32:16 -05:00
|
|
|
|
2019-11-25 20:54:24 -05:00
|
|
|
from dcim.choices import CableLengthUnitChoices
|
2019-12-11 15:26:47 -05:00
|
|
|
from extras.utils import is_taggable
|
2018-02-02 13:32:16 -05:00
|
|
|
|
|
|
|
|
|
|
|
def csv_format(data):
|
|
|
|
"""
|
|
|
|
Encapsulate any data which contains a comma within double quotes.
|
|
|
|
"""
|
|
|
|
csv = []
|
|
|
|
for value in data:
|
|
|
|
|
|
|
|
# Represent None or False with empty string
|
2018-04-12 12:54:21 -04:00
|
|
|
if value is None or value is False:
|
2018-02-02 13:32:16 -05:00
|
|
|
csv.append('')
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Convert dates to ISO format
|
|
|
|
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
|
|
value = value.isoformat()
|
|
|
|
|
|
|
|
# Force conversion to string first so we can check for any commas
|
2018-11-02 15:20:08 -04:00
|
|
|
if not isinstance(value, str):
|
2018-02-02 13:32:16 -05:00
|
|
|
value = '{}'.format(value)
|
|
|
|
|
2020-02-24 13:29:00 -05:00
|
|
|
# Double-quote the value if it contains a comma or line break
|
2018-02-02 16:31:23 -05:00
|
|
|
if ',' in value or '\n' in value:
|
2020-02-24 13:29:00 -05:00
|
|
|
value = value.replace('"', '""') # Escape double-quotes
|
2018-02-02 13:32:16 -05:00
|
|
|
csv.append('"{}"'.format(value))
|
|
|
|
else:
|
|
|
|
csv.append('{}'.format(value))
|
|
|
|
|
|
|
|
return ','.join(csv)
|
|
|
|
|
|
|
|
|
2017-05-15 12:56:16 -04:00
|
|
|
def foreground_color(bg_color):
|
|
|
|
"""
|
|
|
|
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.
|
|
|
|
"""
|
|
|
|
bg_color = bg_color.strip('#')
|
2017-05-15 13:18:49 -04:00
|
|
|
r, g, b = [int(bg_color[c:c + 2], 16) for c in (0, 2, 4)]
|
2017-05-15 12:56:16 -04:00
|
|
|
if r * 0.299 + g * 0.587 + b * 0.114 > 186:
|
|
|
|
return '000000'
|
|
|
|
else:
|
|
|
|
return 'ffffff'
|
2018-05-30 11:19:10 -04:00
|
|
|
|
|
|
|
|
|
|
|
def dynamic_import(name):
|
|
|
|
"""
|
|
|
|
Dynamically import a class from an absolute path string
|
|
|
|
"""
|
|
|
|
components = name.split('.')
|
|
|
|
mod = __import__(components[0])
|
|
|
|
for comp in components[1:]:
|
|
|
|
mod = getattr(mod, comp)
|
|
|
|
return mod
|
2018-06-22 14:00:23 -04:00
|
|
|
|
|
|
|
|
2019-04-19 16:09:22 -04:00
|
|
|
def get_subquery(model, field):
|
|
|
|
"""
|
|
|
|
Return a Subquery suitable for annotating a child object count.
|
|
|
|
"""
|
|
|
|
subquery = Subquery(
|
|
|
|
model.objects.filter(
|
|
|
|
**{field: OuterRef('pk')}
|
|
|
|
).order_by().values(
|
|
|
|
field
|
|
|
|
).annotate(
|
|
|
|
c=Count('*')
|
|
|
|
).values('c')
|
|
|
|
)
|
|
|
|
|
|
|
|
return subquery
|
|
|
|
|
|
|
|
|
2020-02-25 14:48:11 +00:00
|
|
|
def serialize_object(obj, extra=None, exclude=None):
|
2018-06-22 14:00:23 -04:00
|
|
|
"""
|
|
|
|
Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like
|
2020-02-25 14:48:11 +00:00
|
|
|
change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys
|
|
|
|
can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are
|
|
|
|
implicitly excluded.
|
2018-06-22 14:00:23 -04:00
|
|
|
"""
|
|
|
|
json_str = serialize('json', [obj])
|
|
|
|
data = json.loads(json_str)[0]['fields']
|
2018-06-29 10:40:57 -04:00
|
|
|
|
|
|
|
# Include any custom fields
|
|
|
|
if hasattr(obj, 'get_custom_fields'):
|
|
|
|
data['custom_fields'] = {
|
2019-10-22 14:36:30 -04:00
|
|
|
field: str(value) for field, value in obj.cf.items()
|
2018-06-29 10:40:57 -04:00
|
|
|
}
|
|
|
|
|
2018-07-10 10:10:22 -04:00
|
|
|
# Include any tags
|
2019-12-11 15:26:47 -05:00
|
|
|
if is_taggable(obj):
|
2018-08-07 08:52:57 -04:00
|
|
|
data['tags'] = [tag.name for tag in obj.tags.all()]
|
2018-07-10 10:10:22 -04:00
|
|
|
|
2018-06-29 10:40:57 -04:00
|
|
|
# Append any extra data
|
2018-06-22 14:00:23 -04:00
|
|
|
if extra is not None:
|
2018-06-22 15:27:22 -04:00
|
|
|
data.update(extra)
|
2018-06-29 10:40:57 -04:00
|
|
|
|
2020-02-25 14:48:11 +00:00
|
|
|
# Copy keys to list to avoid 'dictionary changed size during iteration' exception
|
|
|
|
for key in list(data):
|
|
|
|
# Private fields shouldn't be logged in the object change
|
|
|
|
if isinstance(key, str) and key.startswith('_'):
|
|
|
|
data.pop(key)
|
|
|
|
|
|
|
|
# Explicitly excluded keys
|
|
|
|
if isinstance(exclude, (list, tuple)) and key in exclude:
|
|
|
|
data.pop(key)
|
|
|
|
|
2018-06-22 14:00:23 -04:00
|
|
|
return data
|
2018-12-05 14:34:49 -05:00
|
|
|
|
|
|
|
|
2019-04-16 18:02:52 -04:00
|
|
|
def dict_to_filter_params(d, prefix=''):
|
|
|
|
"""
|
|
|
|
Translate a dictionary of attributes to a nested set of parameters suitable for QuerySet filtering. For example:
|
|
|
|
|
|
|
|
{
|
|
|
|
"name": "Foo",
|
|
|
|
"rack": {
|
|
|
|
"facility_id": "R101"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Becomes:
|
|
|
|
|
|
|
|
{
|
|
|
|
"name": "Foo",
|
|
|
|
"rack__facility_id": "R101"
|
|
|
|
}
|
|
|
|
|
|
|
|
And can be employed as filter parameters:
|
|
|
|
|
|
|
|
Device.objects.filter(**dict_to_filter(attrs_dict))
|
|
|
|
"""
|
|
|
|
params = {}
|
|
|
|
for key, val in d.items():
|
|
|
|
k = prefix + key
|
|
|
|
if isinstance(val, dict):
|
|
|
|
params.update(dict_to_filter_params(val, k + '__'))
|
|
|
|
else:
|
|
|
|
params[k] = val
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
2018-12-05 14:34:49 -05:00
|
|
|
def deepmerge(original, new):
|
|
|
|
"""
|
|
|
|
Deep merge two dictionaries (new into original) and return a new dict
|
|
|
|
"""
|
|
|
|
merged = OrderedDict(original)
|
|
|
|
for key, val in new.items():
|
|
|
|
if key in original and isinstance(original[key], dict) and isinstance(val, dict):
|
|
|
|
merged[key] = deepmerge(original[key], val)
|
|
|
|
else:
|
|
|
|
merged[key] = val
|
|
|
|
return merged
|
2018-12-07 10:51:28 -05:00
|
|
|
|
|
|
|
|
2018-10-26 10:28:25 -04:00
|
|
|
def to_meters(length, unit):
|
|
|
|
"""
|
|
|
|
Convert the given length to meters.
|
|
|
|
"""
|
|
|
|
length = int(length)
|
|
|
|
if length < 0:
|
|
|
|
raise ValueError("Length must be a positive integer")
|
2019-11-25 20:54:24 -05:00
|
|
|
|
2020-01-09 14:56:33 -05:00
|
|
|
valid_units = CableLengthUnitChoices.values()
|
2019-11-25 20:54:24 -05:00
|
|
|
if unit not in valid_units:
|
|
|
|
raise ValueError(
|
|
|
|
"Unknown unit {}. Must be one of the following: {}".format(unit, ', '.join(valid_units))
|
|
|
|
)
|
|
|
|
|
|
|
|
if unit == CableLengthUnitChoices.UNIT_METER:
|
2018-10-26 10:28:25 -04:00
|
|
|
return length
|
2019-11-25 20:54:24 -05:00
|
|
|
if unit == CableLengthUnitChoices.UNIT_CENTIMETER:
|
2018-10-26 10:28:25 -04:00
|
|
|
return length / 100
|
2019-11-25 20:54:24 -05:00
|
|
|
if unit == CableLengthUnitChoices.UNIT_FOOT:
|
2018-10-26 10:28:25 -04:00
|
|
|
return length * 0.3048
|
2019-11-25 20:54:24 -05:00
|
|
|
if unit == CableLengthUnitChoices.UNIT_INCH:
|
2018-10-26 10:28:25 -04:00
|
|
|
return length * 0.3048 * 12
|
|
|
|
raise ValueError("Unknown unit {}. Must be 'm', 'cm', 'ft', or 'in'.".format(unit))
|
2019-12-31 14:00:55 -05:00
|
|
|
|
|
|
|
|
|
|
|
def render_jinja2(template_code, context):
|
|
|
|
"""
|
|
|
|
Render a Jinja2 template with the provided context. Return the rendered content.
|
|
|
|
"""
|
|
|
|
return Environment().from_string(source=template_code).render(**context)
|
2019-12-06 16:40:39 -05:00
|
|
|
|
|
|
|
|
|
|
|
def prepare_cloned_fields(instance):
|
|
|
|
"""
|
|
|
|
Compile an object's `clone_fields` list into a string of URL query parameters. Tags are automatically cloned where
|
|
|
|
applicable.
|
|
|
|
"""
|
|
|
|
params = {}
|
|
|
|
for field_name in getattr(instance, 'clone_fields', []):
|
|
|
|
field = instance._meta.get_field(field_name)
|
|
|
|
field_value = field.value_from_object(instance)
|
|
|
|
|
|
|
|
# Swap out False with URL-friendly value
|
|
|
|
if field_value is False:
|
|
|
|
field_value = ''
|
|
|
|
|
|
|
|
# Omit empty values
|
|
|
|
if field_value not in (None, ''):
|
|
|
|
params[field_name] = field_value
|
|
|
|
|
|
|
|
# Copy tags
|
2019-12-11 15:26:47 -05:00
|
|
|
if is_taggable(instance):
|
2019-12-06 16:40:39 -05:00
|
|
|
params['tags'] = ','.join([t.name for t in instance.tags.all()])
|
|
|
|
|
|
|
|
# Concatenate parameters into a URL query string
|
|
|
|
param_string = '&'.join(
|
|
|
|
['{}={}'.format(k, v) for k, v in params.items()]
|
|
|
|
)
|
|
|
|
|
|
|
|
return param_string
|
2020-02-05 09:15:48 -05:00
|
|
|
|
|
|
|
|
2020-02-15 22:39:08 +00:00
|
|
|
def shallow_compare_dict(source_dict, destination_dict, exclude=None):
|
|
|
|
"""
|
|
|
|
Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of
|
|
|
|
the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored.
|
|
|
|
"""
|
|
|
|
difference = {}
|
|
|
|
|
|
|
|
for key in destination_dict:
|
|
|
|
if source_dict.get(key) != destination_dict[key]:
|
|
|
|
if isinstance(exclude, (list, tuple)) and key in exclude:
|
|
|
|
continue
|
|
|
|
difference[key] = destination_dict[key]
|
|
|
|
|
|
|
|
return difference
|
2020-04-24 09:50:26 -04:00
|
|
|
|
|
|
|
|
|
|
|
def flatten_dict(d, prefix='', separator='.'):
|
|
|
|
"""
|
|
|
|
Flatten netsted dictionaries into a single level by joining key names with a separator.
|
|
|
|
|
|
|
|
:param d: The dictionary to be flattened
|
|
|
|
:param prefix: Initial prefix (if any)
|
|
|
|
:param separator: The character to use when concatenating key names
|
|
|
|
"""
|
|
|
|
ret = {}
|
|
|
|
for k, v in d.items():
|
|
|
|
key = separator.join([prefix, k]) if prefix else k
|
|
|
|
if type(v) is dict:
|
|
|
|
ret.update(flatten_dict(v, prefix=key))
|
|
|
|
else:
|
|
|
|
ret[key] = v
|
|
|
|
return ret
|