Commit 67324baa by Őry Máté

Merge branch 'feature-node-ordering' into 'master'

Natural ordering of hostnames

ready to merge
parents cab22b32 f31310e6
from collections import deque
from hashlib import sha224
from logging import getLogger
from time import time
......@@ -139,3 +140,91 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
return x
return inner_cache
class HumanSortField(CharField):
"""
A CharField that monitors another field on the same model and sets itself
to a normalized value, which can be used for sensible lexicographycal
sorting for fields containing numerals. (Avoiding technically correct
orderings like [a1, a10, a2], which can be annoying for file or host
names.)
Apart from CharField's default arguments, an argument is requered:
- monitor sets the base field, whose value is normalized.
- maximum_number_length can also be provided, and defaults to 4. If
you have to sort values containing numbers greater than 9999, you
should increase it.
Code is based on carljm's django-model-utils.
"""
def __init__(self, *args, **kwargs):
logger.debug('Initing HumanSortField(%s %s)',
unicode(args), unicode(kwargs))
kwargs.setdefault('default', "")
self.maximum_number_length = kwargs.pop('maximum_number_length', 4)
monitor = kwargs.pop('monitor', None)
if not monitor:
raise TypeError(
'%s requires a "monitor" argument' % self.__class__.__name__)
self.monitor = monitor
kwargs['blank'] = True
super(HumanSortField, self).__init__(*args, **kwargs)
def get_monitored_value(self, instance):
return getattr(instance, self.monitor)
@staticmethod
def _partition(s, pred):
"""Partition a deque of chars to a tuple of a
- string of the longest prefix matching pred,
- string of the longest prefix after the former one not matching,
- deque of the remaining characters.
>>> HumanSortField._partition(deque("1234abc567"),
... lambda s: s.isdigit())
('1234', 'abc', deque(['5', '6', '7']))
>>> HumanSortField._partition(deque("12ab"), lambda s: s.isalpha())
('', '12', deque(['a', 'b']))
"""
match, notmatch = deque(), deque()
while s and pred(s[0]):
match.append(s.popleft())
while s and not pred(s[0]):
notmatch.append(s.popleft())
return (''.join(match), ''.join(notmatch), s)
def get_normalized_value(self, val):
logger.debug('Normalizing value: %s', val)
norm = ""
val = deque(val)
while val:
numbers, letters, val = self._partition(val,
lambda s: s[0].isdigit())
if numbers:
norm += numbers.rjust(self.maximum_number_length, '0')
norm += letters
logger.debug('Normalized value: %s', norm)
return norm
def pre_save(self, model_instance, add):
logger.debug('Pre-saving %s.%s. %s',
model_instance, self.attname, add)
value = self.get_normalized_value(
self.get_monitored_value(model_instance))
setattr(model_instance, self.attname, value[:self.max_length])
return super(HumanSortField, self).pre_save(model_instance, add)
# allow South to handle these fields smoothly
try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules(rules=[
(
(HumanSortField,),
[],
{'monitor': ('monitor', {}),
'maximum_number_length': ('maximum_number_length', {}), }
),
], patterns=['common\.models\.'])
except ImportError:
pass
from collections import deque
from django.test import TestCase
from mock import MagicMock
from .models import TestClass
from ..models import HumanSortField
class MethodCacheTestCase(TestCase):
......@@ -28,3 +33,33 @@ class MethodCacheTestCase(TestCase):
t1.method('a')
self.assertEqual(val1a, val1b)
self.assertEqual(t1.called, 2)
class TestHumanSortField(TestCase):
def test_partition(self):
values = {(lambda s: s.isdigit(), "1234abc56"): ("1234", "abc", "56"),
(lambda s: s.isalpha(), "abc567"): ("abc", "567", ""),
(lambda s: s == "a", "aaababaa"): ("aaa", "b", "abaa"),
(lambda s: s == "a", u"aaababaa"): ("aaa", "b", "abaa"),
}
for (pred, val), result in values.iteritems():
a, b, c = HumanSortField._partition(deque(val), pred)
assert isinstance(c, deque)
c = ''.join(c)
# print "%s, %s => %s" % (val, str(pred), str((a, b, c)))
self.assertEquals((a, b, c), result)
def test_get_normalized(self):
values = {("1234abc56", 4): "1234abc0056",
("abc567", 2): "abc567",
("aaababaa", 8): "aaababaa",
("aa4ababaa", 2): "aa04ababaa",
("aa4aba24baa4", 4): "aa0004aba0024baa0004",
}
for (val, length), result in values.iteritems():
obj = MagicMock(spec=HumanSortField, maximum_number_length=length,
_partition=HumanSortField._partition)
test_result = HumanSortField.get_normalized_value(obj, val)
self.assertEquals(test_result, result)
......@@ -66,7 +66,8 @@ class NodeListTable(Table):
)
name = TemplateColumn(
template_name="dashboard/node-list/column-name.html"
template_name="dashboard/node-list/column-name.html",
order_by="normalized_name"
)
priority = Column(
......
......@@ -17,6 +17,7 @@ import django.conf
from django.db.models.signals import post_save, post_delete
import random
from common.models import HumanSortField
from firewall.tasks.local_tasks import reloadtask
from acl.models import AclBase
logger = logging.getLogger(__name__)
......@@ -387,6 +388,7 @@ class Host(models.Model):
'the host, the first part of '
'the FQDN.'),
validators=[val_alfanum])
normalized_hostname = HumanSortField(monitor='hostname', max_length=80)
reverse = models.CharField(max_length=40, validators=[val_domain],
verbose_name=_('reverse'),
help_text=_('The fully qualified reverse '
......@@ -440,6 +442,7 @@ class Host(models.Model):
class Meta(object):
unique_together = ('hostname', 'vlan')
ordering = ('normalized_hostname', 'vlan')
def __unicode__(self):
return self.hostname
......
......@@ -11,7 +11,7 @@ from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager
from common.models import method_cache, WorkerNotFound
from common.models import method_cache, WorkerNotFound, HumanSortField
from firewall.models import Host
from ..tasks import vm_tasks, local_tasks
from .common import Trait
......@@ -43,6 +43,7 @@ class Node(TimeStampedModel):
name = CharField(max_length=50, unique=True,
verbose_name=_('name'),
help_text=_('Human readable name of node.'))
normalized_name = HumanSortField(monitor='name', max_length=100)
priority = IntegerField(verbose_name=_('priority'),
help_text=_('Node usage priority.'))
host = ForeignKey(Host, verbose_name=_('host'),
......@@ -62,6 +63,7 @@ class Node(TimeStampedModel):
app_label = 'vm'
db_table = 'vm_node'
permissions = ()
ordering = ('-enabled', 'normalized_name')
def __unicode__(self):
return self.name
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment