class Link(BaseAccessLevel): """ Link Model Intended for both wireless and wired links """ interface_a = models.ForeignKey(Interface, verbose_name=_('from interface'), related_name='link_interface_from', blank=True, null=True, help_text=_('mandatory except for "planned" links')) interface_b = models.ForeignKey(Interface, verbose_name=_('to interface'), related_name='link_interface_to', blank=True, null=True, help_text=_('mandatory except for "planned" links')) node_a = models.ForeignKey(Node, verbose_name=_('from node'), related_name='link_node_from', blank=True, null=True, help_text=_('leave blank (except for planned nodes) as it will be filled in automatically if necessary')) node_b = models.ForeignKey(Node, verbose_name=_('to node'), related_name='link_node_to', blank=True, null=True, help_text=_('leave blank (except for planned nodes) as it will be filled in automatically if necessary')) type = models.SmallIntegerField(_('type'), max_length=10, choices=choicify(LINK_TYPE), default=LINK_TYPE.get('radio')) status = models.SmallIntegerField(_('status'), choices=choicify(LINK_STATUS), default=LINK_STATUS.get('planned')) metric_type = models.CharField(_('metric type'), max_length=6, choices=choicify(METRIC_TYPES), blank=True) metric_value = models.FloatField(_('metric value'), blank=True, null=True) tx_rate = models.IntegerField(_('TX rate average'), null=True, default=None, blank=True) rx_rate = models.IntegerField(_('RX rate average'), null=True, default=None, blank=True) dbm = models.IntegerField(_('dBm average'), null=True, default=None, blank=True) noise = models.IntegerField(_('noise average'), null=True, default=None, blank=True) # manager objects = LinkManager() class Meta: permissions = (('can_view_links', 'Can view links'),) def clean(self, *args, **kwargs): """ Custom validation 1. interface_a and interface_b mandatory except for planned links 2. planned links should have at least node_a and node_b filled in 3. dbm and noise fields can be filled only for radio links """ if self.status != LINK_STATUS.get('planned') and (self.interface_a == None or self.interface_b == None): raise ValidationError(_('fields "from interface" and "to interface" are mandatory in this case')) if self.status == LINK_STATUS.get('planned') and (self.node_a == None or self.node_b == None): raise ValidationError(_('fields "from node" and "to node" are mandatory for planned links')) if self.dbm != None or self.noise != None: raise ValidationError(_('Only links of type "radio" can contain "dbm" and "noise" information')) def save(self, *args, **kwargs): """ Automatically fill 'node_a' and 'node_b' fields if necessary """ if self.interface_a != None: self.node_a = self.interface_a.device.node if self.interface_b != None: self.node_b = self.interface_b.device.node super(Link, self).save(*args, **kwargs)
('babel', 'Babel'), ('802.11s', 'Open 802.11s'), ('bgp', 'BGP'), ('ospf', 'OSPF'), ('static', _('Static Routing')), ) DEVICE_TYPES = { 'radio device': 'radio', 'server': 'server', 'router': 'router', 'switch managed': 'switch', 'sensor': 'sensor', 'other': 'other', } DEVICE_TYPES_CHOICES = choicify(DEVICE_TYPES) DEVICE_STATUS = { 'not_reachable': 0, # device is not reachable 'reachable': 1, # device is reachable 'unknown': 2, # device has not been seen by the system yet 'inactive': 3, # manually deactivated by user or admin } DEVICE_STATUS_CHOICES = choicify(DEVICE_STATUS) WIRELESS_MODE = ( ('sta', _('station')), ('ap', _('access point')), ('adhoc', _('adhoc')), ('monitor', _('monitor')), ('mesh', _('mesh')),
'users': str(FILTER_CHOICES[2][0]) } OUTWARD_STATUS_CHOICES = ((-1, _('error')), (0, _('draft')), (1, _('scheduled')), (2, _('sent')), (3, _('cancelled'))) # this is just for convenience and readability OUTWARD_STATUS = { 'error': OUTWARD_STATUS_CHOICES[0][0], 'draft': OUTWARD_STATUS_CHOICES[1][0], 'scheduled': OUTWARD_STATUS_CHOICES[2][0], 'sent': OUTWARD_STATUS_CHOICES[3][0], 'cancelled': OUTWARD_STATUS_CHOICES[4][0] } GROUPS = [] DEFAULT_GROUPS = '' # convert strings to integers for group in choicify(settings.NODESHOT['CHOICES']['ACCESS_LEVELS']): GROUPS += [(int(group[0]), group[1])] DEFAULT_GROUPS += '%s,' % group[0] GROUPS += [(0, _('super users'))] DEFAULT_GROUPS += '0' INWARD_STATUS_CHOICES = ( (-1, _('Error')), (0, _('Not sent yet')), (1, _('Sent')), (2, _('Cancelled')), )
} OUTWARD_STATUS_CHOICES = ((-1, _('error')), (0, _('draft')), (1, _('scheduled')), (2, _('sent')), (3, _('cancelled'))) # this is just for convenience and readability OUTWARD_STATUS = { 'error': OUTWARD_STATUS_CHOICES[0][0], 'draft': OUTWARD_STATUS_CHOICES[1][0], 'scheduled': OUTWARD_STATUS_CHOICES[2][0], 'sent': OUTWARD_STATUS_CHOICES[3][0], 'cancelled': OUTWARD_STATUS_CHOICES[4][0] } GROUPS = [] DEFAULT_GROUPS = '' # convert strings to integers for group in choicify(ACCESS_LEVELS): GROUPS += [(int(group[0]), group[1])] DEFAULT_GROUPS += '%s,' % group[0] GROUPS += [(0, _('super users'))] DEFAULT_GROUPS += '0' INWARD_STATUS_CHOICES = ( (-1, _('Error')), (0, _('Not sent yet')), (1, _('Sent')), (2, _('Cancelled')), )
(0, _('draft')), (1, _('scheduled')), (2, _('sent')), (3, _('cancelled')) ) # this is just for convenience and readability OUTWARD_STATUS = { 'error': OUTWARD_STATUS_CHOICES[0][0], 'draft': OUTWARD_STATUS_CHOICES[1][0], 'scheduled': OUTWARD_STATUS_CHOICES[2][0], 'sent': OUTWARD_STATUS_CHOICES[3][0], 'cancelled': OUTWARD_STATUS_CHOICES[4][0] } GROUPS = [] DEFAULT_GROUPS = '' # convert strings to integers for group in choicify(settings.NODESHOT['CHOICES']['ACCESS_LEVELS']): GROUPS += [(int(group[0]), group[1])] DEFAULT_GROUPS += '%s,' % group[0] GROUPS += [(0, _('super users'))] DEFAULT_GROUPS += '0' INWARD_STATUS_CHOICES = ( (-1, _('Error')), (0, _('Not sent yet')), (1, _('Sent')), (2, _('Cancelled')), )
from django.utils.translation import ugettext_lazy as _ from nodeshot.core.base.utils import choicify POLARIZATIONS = { 'horizonal': 1, 'vertical': 2, 'circular': 3, 'linear': 4, 'dual_linear': 5 } POLARIZATION_CHOICES = choicify(POLARIZATIONS)
from django.utils.translation import ugettext_lazy as _ from nodeshot.core.base.utils import choicify SEX = { 'male': 'M', 'female': 'F' } SEX_CHOICES = choicify(SEX)
class Link(BaseAccessLevel): """ Link Model Designed for both wireless and wired links """ type = models.SmallIntegerField(_('type'), max_length=10, null=True, blank=True, choices=choicify(LINK_TYPES), default=LINK_TYPES.get('radio')) # in most cases these two fields are mandatory, except for "planned" links interface_a = models.ForeignKey( Interface, verbose_name=_('from interface'), related_name='link_interface_from', blank=True, null=True, help_text= _('mandatory except for "planned" links (in planned links you might not have any device installed yet)' )) interface_b = models.ForeignKey( Interface, verbose_name=_('to interface'), related_name='link_interface_to', blank=True, null=True, help_text= _('mandatory except for "planned" links (in planned links you might not have any device installed yet)' )) # in "planned" links these two fields are necessary # while in all the other status they serve as a shortcut node_a = models.ForeignKey( Node, verbose_name=_('from node'), related_name='link_node_from', blank=True, null=True, help_text= _('leave blank (except for planned nodes) as it will be filled in automatically' )) node_b = models.ForeignKey( Node, verbose_name=_('to node'), related_name='link_node_to', blank=True, null=True, help_text= _('leave blank (except for planned nodes) as it will be filled in automatically' )) # shortcut layer = models.ForeignKey( Layer, verbose_name=_('layer'), blank=True, null=True, help_text=_('leave blank - it will be filled in automatically')) # geospatial info line = models.LineStringField( blank=True, null=True, help_text=_('leave blank and the line will be drawn automatically')) # monitoring info status = models.SmallIntegerField(_('status'), choices=choicify(LINK_STATUS), default=LINK_STATUS.get('planned')) first_seen = models.DateTimeField(_('first time seen on'), blank=True, null=True, default=None) last_seen = models.DateTimeField(_('last time seen on'), blank=True, null=True, default=None) # technical info metric_type = models.CharField(_('metric type'), max_length=6, choices=choicify(METRIC_TYPES), blank=True, null=True) metric_value = models.FloatField(_('metric value'), blank=True, null=True) max_rate = models.IntegerField(_('Maximum BPS'), null=True, default=None, blank=True) min_rate = models.IntegerField(_('Minimum BPS'), null=True, default=None, blank=True) # wireless specific info dbm = models.IntegerField(_('dBm average'), null=True, default=None, blank=True) noise = models.IntegerField(_('noise average'), null=True, default=None, blank=True) # additional data data = DictionaryField( _('extra data'), null=True, blank=True, help_text=_('store extra attributes in JSON string')) shortcuts = ReferencesField(null=True, blank=True) # django manager objects = LinkManager() class Meta: app_label = 'links' def __unicode__(self): return _(u'%s <> %s') % (self.node_a_name, self.node_b_name) def clean(self, *args, **kwargs): """ Custom validation 1. interface_a and interface_b mandatory except for planned links 2. planned links should have at least node_a and node_b filled in 3. dbm and noise fields can be filled only for radio links 4. interface_a and interface_b must differ 5. interface a and b type must match """ if self.status != LINK_STATUS.get('planned') and ( self.interface_a is None or self.interface_b is None): raise ValidationError( _('fields "from interface" and "to interface" are mandatory in this case' )) if self.status == LINK_STATUS.get('planned') and ( self.node_a is None or self.node_b is None): raise ValidationError( _('fields "from node" and "to node" are mandatory for planned links' )) if self.type != LINK_TYPES.get('radio') and (self.dbm is not None or self.noise is not None): raise ValidationError( _('Only links of type "radio" can contain "dbm" and "noise" information' )) if (self.interface_a_id == self.interface_b_id) or (self.interface_a == self.interface_b): raise ValidationError( _('link cannot have same "from interface" and "to interface"')) if (self.interface_a and self.interface_b ) and self.interface_a.type != self.interface_b.type: format_tuple = (self.interface_a.get_type_display(), self.interface_b.get_type_display()) raise ValidationError( _('link cannot be between of interfaces of different types:\ interface a is "%s" while b is "%s"') % format_tuple) def save(self, *args, **kwargs): """ Custom save does the following: * determine link type if not specified * automatically fill 'node_a' and 'node_b' fields if necessary * draw line between two nodes * fill shortcut properties node_a_name and node_b_name """ if not self.type: if self.interface_a.type == INTERFACE_TYPES.get('wireless'): self.type = LINK_TYPES.get('radio') elif self.interface_a.type == INTERFACE_TYPES.get('ethernet'): self.type = LINK_TYPES.get('ethernet') else: self.type = LINK_TYPES.get('virtual') if self.interface_a_id: self.interface_a = Interface.objects.get(pk=self.interface_a_id) if self.interface_b_id: self.interface_b = Interface.objects.get(pk=self.interface_b_id) # fill in node_a and node_b if self.node_a is None and self.interface_a is not None: self.node_a = self.interface_a.node if self.node_b is None and self.interface_b is not None: self.node_b = self.interface_b.node # fill layer from node_a if self.layer is None: self.layer = self.node_a.layer # draw linestring if not self.line: self.line = LineString(self.node_a.point, self.node_b.point) # fill properties if self.data is None or self.data.get('node_a_name', None) is None: self.data = self.data or {} # in case is None init empty dict self.data['node_a_name'] = self.node_a.name self.data['node_b_name'] = self.node_b.name if self.data.get('node_a_slug', None) is None or self.data.get( 'node_b_slug', None) is None: self.data['node_a_slug'] = self.node_a.slug self.data['node_b_slug'] = self.node_b.slug if self.data.get('interface_a_mac', None) is None or self.data.get( 'interface_b_mac', None) is None: self.data['interface_a_mac'] = self.interface_a.mac self.data['interface_b_mac'] = self.interface_b.mac if self.data.get('layer_slug') != self.layer.slug: self.data['layer_slug'] = self.layer.slug super(Link, self).save(*args, **kwargs) @property def node_a_name(self): self.data = self.data or {} return self.data.get('node_a_name', None) @property def node_b_name(self): self.data = self.data or {} return self.data.get('node_b_name', None) @property def node_a_slug(self): self.data = self.data or {} return self.data.get('node_a_slug', None) @property def node_b_slug(self): self.data = self.data or {} return self.data.get('node_b_slug', None) @property def interface_a_mac(self): self.data = self.data or {} return self.data.get('interface_a_mac', None) @property def interface_b_mac(self): self.data = self.data or {} return self.data.get('interface_b_mac', None) @property def layer_slug(self): self.data = self.data or {} return self.data.get('layer_slug', None) @property def quality(self): """ Quality is a number between 1 and 6 that rates the quality of the link. The way quality is calculated might be overridden by settings. 0 means unknown """ if self.metric_value is None: return 0 # PLACEHOLDER return 6
from nodeshot.core.base.utils import choicify POLARIZATIONS = { 'horizonal': 1, 'vertical': 2, 'circular': 3, 'linear': 4, 'dual_linear': 5 } POLARIZATION_CHOICES = choicify(POLARIZATIONS)
'onu': 'onu', 'other': 'other', 'phone': 'phone', 'ppanel': 'ppanel', 'rack': 'rack', 'radio device': 'radio', 'router': 'router', 'sensor': 'sensor', 'server': 'server', 'solar': 'solar', 'splitter': 'splitter', 'switch managed': 'switch', 'torpedo': 'torpedo', 'ups': 'ups', } DEVICE_TYPES_CHOICES = choicify(DEVICE_TYPES) DEVICE_STATUS = { 'not_reachable': 0, # device is not reachable 'reachable': 1, # device is reachable 'unknown': 2, # device has not been seen by the system yet 'inactive': 3, # manually deactivated by user or admin } DEVICE_STATUS_CHOICES = choicify(DEVICE_STATUS) WIRELESS_MODE = ( ('sta', _('station')), ('ap', _('access point')), ('adhoc', _('adhoc')), ('monitor', _('monitor')), ('mesh', _('mesh')),
class Link(BaseAccessLevel): """ Link Model Designed for both wireless and wired links """ type = models.SmallIntegerField(_('type'), null=True, blank=True, choices=choicify(LINK_TYPES), default=LINK_TYPES.get('radio')) # in most cases these two fields are mandatory, except for "planned" links interface_a = models.ForeignKey( Interface, verbose_name=_('from interface'), related_name='link_interface_from', blank=True, null=True, help_text= _('mandatory except for "planned" links (in planned links you might not have any device installed yet)' )) interface_b = models.ForeignKey( Interface, verbose_name=_('to interface'), related_name='link_interface_to', blank=True, null=True, help_text= _('mandatory except for "planned" links (in planned links you might not have any device installed yet)' )) topology = models.ForeignKey( Topology, blank=True, null=True, help_text=_('mandatory to draw the link dinamically')) # in "planned" links these two fields are necessary # while in all the other status they serve as a shortcut node_a = models.ForeignKey( Node, verbose_name=_('from node'), related_name='link_node_from', blank=True, null=True, help_text= _('leave blank (except for planned nodes) as it will be filled in automatically' )) node_b = models.ForeignKey( Node, verbose_name=_('to node'), related_name='link_node_to', blank=True, null=True, help_text= _('leave blank (except for planned nodes) as it will be filled in automatically' )) # shortcut layer = models.ForeignKey( Layer, verbose_name=_('layer'), blank=True, null=True, help_text=_('leave blank - it will be filled in automatically')) # geospatial info line = models.LineStringField( blank=True, null=True, help_text=_('leave blank and the line will be drawn automatically')) # monitoring info status = models.SmallIntegerField(_('status'), choices=choicify(LINK_STATUS), default=LINK_STATUS.get('planned')) first_seen = models.DateTimeField(_('first time seen on'), blank=True, null=True, default=None) last_seen = models.DateTimeField(_('last time seen on'), blank=True, null=True, default=None) # technical info metric_type = models.CharField(_('metric type'), max_length=6, choices=choicify(METRIC_TYPES), blank=True, null=True) metric_value = models.FloatField(_('metric value'), blank=True, null=True) max_rate = models.IntegerField(_('Maximum BPS'), null=True, default=None, blank=True) min_rate = models.IntegerField(_('Minimum BPS'), null=True, default=None, blank=True) # wireless specific info dbm = models.IntegerField(_('dBm average'), null=True, default=None, blank=True) noise = models.IntegerField(_('noise average'), null=True, default=None, blank=True) # additional data data = DictionaryField( _('extra data'), null=True, blank=True, help_text=_('store extra attributes in JSON string')) shortcuts = ReferencesField(null=True, blank=True) # django manager objects = LinkManager() class Meta: app_label = 'links' def __unicode__(self): return _(u'%s <> %s') % (self.node_a_name, self.node_b_name) def clean(self, *args, **kwargs): """ Custom validation 1. interface_a and interface_b mandatory except for planned links 2. planned links should have at least node_a and node_b filled in 3. dbm and noise fields can be filled only for radio links 4. interface_a and interface_b must differ 5. interface a and b type must match """ if self.status != LINK_STATUS.get('planned'): if self.interface_a is None or self.interface_b is None: raise ValidationError( _('fields "from interface" and "to interface" are mandatory in this case' )) if (self.interface_a_id == self.interface_b_id) or (self.interface_a == self.interface_b): msg = _( 'link cannot have same "from interface" and "to interface: %s"' ) % self.interface_a raise ValidationError(msg) if self.status == LINK_STATUS.get('planned') and ( self.node_a is None or self.node_b is None): raise ValidationError( _('fields "from node" and "to node" are mandatory for planned links' )) if self.type != LINK_TYPES.get('radio') and (self.dbm is not None or self.noise is not None): raise ValidationError( _('Only links of type "radio" can contain "dbm" and "noise" information' )) def save(self, *args, **kwargs): """ Custom save does the following: * determine link type if not specified * automatically fill 'node_a' and 'node_b' fields if necessary * draw line between two nodes * fill shortcut properties node_a_name and node_b_name """ if not self.type: if self.interface_a.type == INTERFACE_TYPES.get('wireless'): self.type = LINK_TYPES.get('radio') elif self.interface_a.type == INTERFACE_TYPES.get('ethernet'): self.type = LINK_TYPES.get('ethernet') else: self.type = LINK_TYPES.get('virtual') if self.interface_a_id: self.interface_a = Interface.objects.get(pk=self.interface_a_id) if self.interface_b_id: self.interface_b = Interface.objects.get(pk=self.interface_b_id) # fill in node_a and node_b if self.node_a is None and self.interface_a is not None: self.node_a = self.interface_a.node if self.node_b is None and self.interface_b is not None: self.node_b = self.interface_b.node # fill layer from node_a if self.layer is None: self.layer = self.node_a.layer # draw linestring if not self.line: self.line = LineString(self.node_a.point, self.node_b.point) # fill properties if self.data.get('node_a_name', None) is None: self.data['node_a_name'] = self.node_a.name self.data['node_b_name'] = self.node_b.name if self.data.get('node_a_slug', None) is None or self.data.get( 'node_b_slug', None) is None: self.data['node_a_slug'] = self.node_a.slug self.data['node_b_slug'] = self.node_b.slug if self.interface_a and self.data.get('interface_a_mac', None) is None: self.data['interface_a_mac'] = self.interface_a.mac if self.interface_b and self.data.get('interface_b_mac', None) is None: self.data['interface_b_mac'] = self.interface_b.mac if self.data.get('layer_slug') != self.layer.slug: self.data['layer_slug'] = self.layer.slug super(Link, self).save(*args, **kwargs) @classmethod def get_link(cls, source, target, topology=None): """ Find link between source and target, (or vice versa, order is irrelevant). :param source: ip or mac addresses :param target: ip or mac addresses :param topology: optional topology relation :returns: Link object :raises: LinkNotFound """ a = source b = target # ensure parameters are coherent if not (valid_ipv4(a) and valid_ipv4(b)) and not ( valid_ipv6(a) and valid_ipv6(b)) and not (valid_mac(a) and valid_mac(b)): raise ValueError('Expecting valid ipv4, ipv6 or mac address') # get interfaces a = cls._get_link_interface(a) b = cls._get_link_interface(b) # raise LinkDataNotFound if an interface is not found not_found = [] if a is None: not_found.append(source) if b is None: not_found.append(target) if not_found: msg = 'the following interfaces could not be found: {0}'.format( ', '.join(not_found)) raise LinkDataNotFound(msg) # find link with interfaces # inverse order is also ok q = (Q(interface_a=a, interface_b=b) | Q(interface_a=b, interface_b=a)) # add topology to lookup if topology: q = q & Q(topology=topology) link = Link.objects.filter(q).first() if link is None: raise LinkNotFound('Link matching query does not exist', interface_a=a, interface_b=b, topology=topology) return link @classmethod def _get_link_interface(self, string_id): if valid_ipv4(string_id) or valid_ipv6(string_id): try: return Ip.objects.get(address=string_id).interface except Ip.DoesNotExist as e: return None else: try: return Interface.objects.get(mac=string_id) except Interface.DoesNotExist as e: return None @classmethod def get_or_create(cls, source, target, cost, topology=None): """ Tries to find a link with get_link, creates a new link if link not found. """ try: return cls.get_link(source, target, topology) except LinkNotFound as e: pass # create link link = Link(interface_a=e.interface_a, interface_b=e.interface_b, status=LINK_STATUS['active'], metric_value=cost, topology=topology) link.full_clean() link.save() return link @property def node_a_name(self): return self.data.get('node_a_name', None) @property def node_b_name(self): return self.data.get('node_b_name', None) @property def node_a_slug(self): return self.data.get('node_a_slug', None) @property def node_b_slug(self): return self.data.get('node_b_slug', None) @property def interface_a_mac(self): return self.data.get('interface_a_mac', None) @property def interface_b_mac(self): return self.data.get('interface_b_mac', None) @property def layer_slug(self): return self.data.get('layer_slug', None) @property def quality(self): """ Quality is a number between 1 and 6 that rates the quality of the link. The way quality is calculated might be overridden by settings. 0 means unknown """ if self.metric_value is None: return 0 # PLACEHOLDER return 6 def ensure(self, status, cost): """ ensure link properties correspond to the specified ones perform save operation only if necessary """ changed = False status_id = LINK_STATUS[status] if self.status != status_id: self.status = status_id changed = True if self.metric_value != cost: self.metric_value = cost changed = True if changed: self.save()
class Node(BaseAccessLevel): """ Nodes of a network, can be assigned to 'Layers' and should belong to 'Users' """ name = models.CharField(_('name'), max_length=50, unique=True) slug = models.SlugField(max_length=50, db_index=True, unique=True) address = models.CharField(_('address'), max_length=150, blank=True, null=True) status = models.SmallIntegerField( _('status'), max_length=3, choices=choicify(NODE_STATUS), default=NODE_STATUS.get(settings.NODESHOT['DEFAULTS']['NODE_STATUS'], 'potential')) is_published = models.BooleanField( default=settings.NODESHOT['DEFAULTS'].get('NODE_PUBLISHED', True)) if 'nodeshot.core.layers' in settings.INSTALLED_APPS: # layer might need to be able to be blank, would require custom validation layer = models.ForeignKey('layers.Layer') if 'nodeshot.interoperability' in settings.INSTALLED_APPS: # add reference to the external layer's ID external_id = models.PositiveIntegerField(blank=True, null=True) # nodes might be assigned to a foreign layer, therefore user can be left blank, requires custom validation user = models.ForeignKey(User, blank=True, null=True) # area if enabled if settings.NODESHOT['SETTINGS']['NODE_AREA']: area = models.PolygonField(blank=True, null=True) # positioning coords = models.PointField(_('coordinates')) elev = models.FloatField(_('elevation'), blank=True, null=True) description = models.TextField(_('description'), max_length=255, blank=True, null=True) notes = models.TextField(_('notes'), blank=True, null=True) # manager objects = GeoAccessLevelPublishedManager() # this is needed to check if the status is changing # explained here: # http://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed _current_status = None class Meta: db_table = 'nodes_node' app_label = 'nodes' permissions = (('can_view_nodes', 'Can view nodes'), ) if 'nodeshot.interoperability' in settings.INSTALLED_APPS: # the combinations of layer_id and external_id must be unique unique_together = ('layer', 'external_id') def __unicode__(self): return '%s' % self.name def __init__(self, *args, **kwargs): """ Fill __current_status """ super(Node, self).__init__(*args, **kwargs) # set current status, but only if it is an existing node if self.pk: self._current_status = self.status def clean(self, *args, **kwargs): #TODO: write test """ Check distance between nodes if feature is enabled. Check node is contained, in layer's area if defined """ distance = settings.NODESHOT['DEFAULTS']['ZONE_MINIMUM_DISTANCE'] coords = self.coords layer_area = self.layer.area near_nodes = Node.objects.filter(coords__distance_lte=(coords, D(m=distance))) error_string_distance = "Distance between nodes cannot be less than %s meters" % ( distance) error_string_not_contained_in_layer = "Node must be inside layer area" if len(near_nodes) > 0: raise ValidationError(error_string_distance) if layer_area is not None and not layer_area.contains(coords): raise ValidationError(error_string_not_contained_in_layer) def save(self, *args, **kwargs): super(Node, self).save(*args, **kwargs) # if status of a node changes if self.status != self._current_status: # send django signal node_status_changed.send(sender=self, old_status=self._current_status, new_status=self.status) # update __current_status self._current_status = self.status if 'grappelli' in settings.INSTALLED_APPS: @staticmethod def autocomplete_search_fields(): return ('name__icontains', )
class Layer(BaseDate): """ Layer Model """ name = models.CharField(_('name'), max_length=50, unique=True) slug = models.SlugField(max_length=50, db_index=True, unique=True) description = models.CharField(_('description'), max_length=250, blank=True, null=True) # record management is_published = models.BooleanField(_('published'), default=True) is_external = models.BooleanField(_('is it external?')) # geographic related fields center = models.PointField(_('center coordinates'), null=True, blank=True) area = models.PolygonField(_('area'), null=True, blank=True) zoom = models.SmallIntegerField( _('default zoom level'), choices=MAP_ZOOM, default=settings.NODESHOT['DEFAULTS']['ZONE_ZOOM']) # organizational organization = models.CharField( _('organization'), help_text=_('Organization which is responsible to manage this layer'), max_length=255) website = models.URLField(_('Website'), blank=True, null=True) email = models.EmailField( _('email'), help_text= _('possibly an email address that delivers messages to all the active participants; if you don\'t have such an email you can add specific users in the "mantainers" field' ), blank=True) mantainers = models.ManyToManyField( User, verbose_name=_('mantainers'), help_text= _('you can specify the users who are mantaining this layer so they will receive emails from the system' ), blank=True) # settings minimum_distance = models.IntegerField( default=settings.NODESHOT['DEFAULTS']['ZONE_MINIMUM_DISTANCE'], help_text=_( 'minimum distance between nodes, 0 means feature disabled')) write_access_level = models.SmallIntegerField( _('write access level'), choices=choicify(ACCESS_LEVELS), default=ACCESS_LEVELS.get('public'), help_text=_('minimum access level to insert new nodes in this layer')) # default manager objects = LayerManager() class Meta: db_table = 'layers_layer' app_label = 'layers' def __unicode__(self): return '%s' % self.name if 'grappelli' in settings.INSTALLED_APPS: @staticmethod def autocomplete_search_fields(): return ('name__icontains', 'slug__icontains') def clean(self, *args, **kwargs): """ Custom validation """ pass
(0, _('draft')), (1, _('scheduled')), (2, _('sent')), (3, _('cancelled')) ) # this is just for convenience and readability OUTWARD_STATUS = { 'error': OUTWARD_STATUS_CHOICES[0][0], 'draft': OUTWARD_STATUS_CHOICES[1][0], 'scheduled': OUTWARD_STATUS_CHOICES[2][0], 'sent': OUTWARD_STATUS_CHOICES[3][0], 'cancelled': OUTWARD_STATUS_CHOICES[4][0] } GROUPS = [] DEFAULT_GROUPS = '' # convert strings to integers for group in choicify(ACCESS_LEVELS): GROUPS += [(int(group[0]), group[1])] DEFAULT_GROUPS += '%s,' % group[0] GROUPS += [(0, _('super users'))] DEFAULT_GROUPS += '0' INWARD_STATUS_CHOICES = ( (-1, _('Error')), (0, _('Not sent yet')), (1, _('Sent')), (2, _('Cancelled')), )
from django.utils.translation import ugettext_lazy as _ from nodeshot.core.base.utils import choicify SEX = {'male': 'M', 'female': 'F'} SEX_CHOICES = choicify(SEX)