mirror of
				https://github.com/netbox-community/netbox.git
				synced 2024-05-10 07:54:54 +00:00 
			
		
		
		
	Minimal implemtnation of custom fields
This commit is contained in:
		@@ -10,7 +10,10 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
 | 
			
		||||
@admin.register(CustomField)
 | 
			
		||||
class CustomFieldAdmin(admin.ModelAdmin):
 | 
			
		||||
    inlines = [CustomFieldChoiceAdmin]
 | 
			
		||||
    list_display = ['name', 'type', 'required', 'default', 'description']
 | 
			
		||||
    list_display = ['name', 'models', 'type', 'required', 'default', 'description']
 | 
			
		||||
 | 
			
		||||
    def models(self, obj):
 | 
			
		||||
        return ', '.join([ct.name for ct in obj.obj_type.all()])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Graph)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +1,38 @@
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
 | 
			
		||||
from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_TEXT, CustomField
 | 
			
		||||
from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CustomField, CustomFieldValue
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomFieldForm(forms.ModelForm):
 | 
			
		||||
    test_field = forms.IntegerField(widget=forms.HiddenInput())
 | 
			
		||||
 | 
			
		||||
    custom_fields = []
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        super(CustomFieldForm, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Find all CustomFields for this model
 | 
			
		||||
        model = self._meta.model
 | 
			
		||||
        custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))
 | 
			
		||||
        obj_type = ContentType.objects.get_for_model(self._meta.model)
 | 
			
		||||
 | 
			
		||||
        # Find all CustomFields for this model
 | 
			
		||||
        custom_fields = CustomField.objects.filter(obj_type=obj_type)
 | 
			
		||||
        for cf in custom_fields:
 | 
			
		||||
 | 
			
		||||
            field_name = 'cf_{}'.format(str(cf.name))
 | 
			
		||||
 | 
			
		||||
            # Integer
 | 
			
		||||
            if cf.type == CF_TYPE_INTEGER:
 | 
			
		||||
                field = forms.IntegerField(blank=not cf.required)
 | 
			
		||||
                field = forms.IntegerField(required=cf.required, initial=cf.default)
 | 
			
		||||
 | 
			
		||||
            # Boolean
 | 
			
		||||
            elif cf.type == CF_TYPE_BOOLEAN:
 | 
			
		||||
                if cf.required:
 | 
			
		||||
                    field = forms.BooleanField(required=False)
 | 
			
		||||
                    field = forms.BooleanField(required=False, initial=bool(cf.default))
 | 
			
		||||
                else:
 | 
			
		||||
                    field = forms.NullBooleanField(required=False)
 | 
			
		||||
                    field = forms.NullBooleanField(required=False, initial=bool(cf.default))
 | 
			
		||||
 | 
			
		||||
            # Date
 | 
			
		||||
            elif cf.type == CF_TYPE_DATE:
 | 
			
		||||
                field = forms.DateField(blank=not cf.required)
 | 
			
		||||
                field = forms.DateField(required=cf.required, initial=cf.default)
 | 
			
		||||
 | 
			
		||||
            # Select
 | 
			
		||||
            elif cf.type == CF_TYPE_SELECT:
 | 
			
		||||
@@ -44,9 +40,50 @@ class CustomFieldForm(forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
            # Text
 | 
			
		||||
            else:
 | 
			
		||||
                field = forms.CharField(max_length=100, blank=not cf.required)
 | 
			
		||||
                field = forms.CharField(max_length=100, required=cf.required, initial=cf.default)
 | 
			
		||||
 | 
			
		||||
            field.model = cf
 | 
			
		||||
            field.label = cf.label if cf.label else cf.name
 | 
			
		||||
            field.help_text = cf.description
 | 
			
		||||
            self.fields[field_name] = field
 | 
			
		||||
            self.custom_fields.append(field_name)
 | 
			
		||||
 | 
			
		||||
        # If editing an existing object, initialize values for all custom fields
 | 
			
		||||
        if self.instance.pk:
 | 
			
		||||
            existing_values = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=self.instance.pk)\
 | 
			
		||||
                .select_related('field')
 | 
			
		||||
            for cfv in existing_values:
 | 
			
		||||
                self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.value
 | 
			
		||||
 | 
			
		||||
    def _save_custom_fields(self):
 | 
			
		||||
 | 
			
		||||
        if self.instance.pk:
 | 
			
		||||
            obj_type = ContentType.objects.get_for_model(self.instance)
 | 
			
		||||
 | 
			
		||||
            for field_name in self.custom_fields:
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    cfv = CustomFieldValue.objects.get(field=self.fields[field_name].model, obj_type=obj_type,
 | 
			
		||||
                                                       obj_id=self.instance.pk)
 | 
			
		||||
                except CustomFieldValue.DoesNotExist:
 | 
			
		||||
                    cfv = CustomFieldValue(
 | 
			
		||||
                        field=self.fields[field_name].model,
 | 
			
		||||
                        obj_type=obj_type,
 | 
			
		||||
                        obj_id=self.instance.pk
 | 
			
		||||
                    )
 | 
			
		||||
                if cfv.pk and self.cleaned_data[field_name] is None:
 | 
			
		||||
                    cfv.delete()
 | 
			
		||||
                elif self.cleaned_data[field_name] is not None:
 | 
			
		||||
                    cfv.value = self.cleaned_data[field_name]
 | 
			
		||||
                    cfv.save()
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        obj = super(CustomFieldForm, self).save(commit)
 | 
			
		||||
 | 
			
		||||
        # Handle custom fields the same way we do M2M fields
 | 
			
		||||
        if commit:
 | 
			
		||||
            self._save_custom_fields()
 | 
			
		||||
        else:
 | 
			
		||||
            self.save_custom_fields = self._save_custom_fields
 | 
			
		||||
 | 
			
		||||
        return obj
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.10 on 2016-08-12 19:52
 | 
			
		||||
# Generated by Django 1.10 on 2016-08-15 19:18
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
@@ -20,11 +20,11 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('type', models.PositiveSmallIntegerField(choices=[(100, b'Text'), (200, b'Integer'), (300, b'Boolean (true/false)'), (400, b'Date'), (500, b'Selection')], default=100)),
 | 
			
		||||
                ('name', models.CharField(max_length=50, unique=True)),
 | 
			
		||||
                ('label', models.CharField(help_text=b'Name of the field as displayed to users', max_length=50)),
 | 
			
		||||
                ('label', models.CharField(blank=True, help_text=b"Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
 | 
			
		||||
                ('description', models.CharField(blank=True, max_length=100)),
 | 
			
		||||
                ('required', models.BooleanField(default=False, help_text=b'This field is required when creating new objects')),
 | 
			
		||||
                ('default', models.CharField(blank=True, help_text=b'Default value for the field', max_length=100)),
 | 
			
		||||
                ('obj_type', models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType')),
 | 
			
		||||
                ('required', models.BooleanField(default=False, help_text=b'Determines whether this field is required when creating new objects or editing an existing object.')),
 | 
			
		||||
                ('default', models.CharField(blank=True, help_text=b'Default value for the field. N/A for selection fields.', max_length=100)),
 | 
			
		||||
                ('obj_type', models.ManyToManyField(help_text=b'The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name=b'Object(s)')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ['name'],
 | 
			
		||||
@@ -57,6 +57,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
                'ordering': ['obj_type', 'obj_id'],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='customfieldvalue',
 | 
			
		||||
            unique_together=set([('field', 'obj_type', 'obj_id')]),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='customfieldchoice',
 | 
			
		||||
            unique_together=set([('field', 'value')]),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,8 @@ from django.http import HttpResponse
 | 
			
		||||
from django.template import Template, Context
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
 | 
			
		||||
from dcim.models import Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: Any model added here MUST have a GenericRelation defined for CustomField
 | 
			
		||||
CUSTOMFIELD_MODELS = (
 | 
			
		||||
    'site', 'rack', 'device',                               # DCIM
 | 
			
		||||
    'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf',      # IPAM
 | 
			
		||||
@@ -62,21 +61,42 @@ ACTION_CHOICES = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomFieldModel(object):
 | 
			
		||||
 | 
			
		||||
    def custom_fields(self):
 | 
			
		||||
 | 
			
		||||
        # Find all custom fields applicable to this type of object
 | 
			
		||||
        content_type = ContentType.objects.get_for_model(self)
 | 
			
		||||
        fields = CustomField.objects.filter(obj_type=content_type)
 | 
			
		||||
 | 
			
		||||
        # If the object exists, populate its custom fields with values
 | 
			
		||||
        if hasattr(self, 'pk'):
 | 
			
		||||
            values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
 | 
			
		||||
            values_dict = {cfv.field_id: cfv.value for cfv in values}
 | 
			
		||||
            return {field: values_dict.get(field.pk) for field in fields}
 | 
			
		||||
        else:
 | 
			
		||||
            return {field: None for field in fields}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomField(models.Model):
 | 
			
		||||
    obj_type = models.ManyToManyField(ContentType, related_name='custom_fields',
 | 
			
		||||
                                      limit_choices_to={'model__in': CUSTOMFIELD_MODELS})
 | 
			
		||||
    obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
 | 
			
		||||
                                      limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
 | 
			
		||||
                                      help_text="The object(s) to which this field applies.")
 | 
			
		||||
    type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
 | 
			
		||||
    name = models.CharField(max_length=50, unique=True)
 | 
			
		||||
    label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users")
 | 
			
		||||
    label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
 | 
			
		||||
                                                                  "provided, the field's name will be used)")
 | 
			
		||||
    description = models.CharField(max_length=100, blank=True)
 | 
			
		||||
    required = models.BooleanField(default=False, help_text="This field is required when creating new objects")
 | 
			
		||||
    default = models.CharField(max_length=100, blank=True, help_text="Default value for the field")
 | 
			
		||||
    required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
 | 
			
		||||
                                                            "new objects or editing an existing object.")
 | 
			
		||||
    default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. N/A for selection "
 | 
			
		||||
                                                                     "fields.")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ['name']
 | 
			
		||||
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        return self.label or self.name
 | 
			
		||||
        return self.label or self.name.capitalize()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomFieldValue(models.Model):
 | 
			
		||||
@@ -90,9 +110,10 @@ class CustomFieldValue(models.Model):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ['obj_type', 'obj_id']
 | 
			
		||||
        unique_together = ['field', 'obj_type', 'obj_id']
 | 
			
		||||
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        return self.value
 | 
			
		||||
        return '{} {}'.format(self.obj, self.field)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
@@ -103,17 +124,19 @@ class CustomFieldValue(models.Model):
 | 
			
		||||
        if self.field.type == CF_TYPE_DATE:
 | 
			
		||||
            return self.val_date
 | 
			
		||||
        if self.field.type == CF_TYPE_SELECT:
 | 
			
		||||
            return CustomFieldChoice.objects.get(pk=self.val_int)
 | 
			
		||||
            return CustomFieldChoice.objects.get(pk=self.val_int) if self.val_int else None
 | 
			
		||||
        return self.val_char
 | 
			
		||||
 | 
			
		||||
    @value.setter
 | 
			
		||||
    def value(self, value):
 | 
			
		||||
        if self.field.type in [CF_TYPE_INTEGER, CF_TYPE_SELECT]:
 | 
			
		||||
        if self.field.type == CF_TYPE_INTEGER:
 | 
			
		||||
            self.val_int = value
 | 
			
		||||
        elif self.field.type == CF_TYPE_BOOLEAN:
 | 
			
		||||
            self.val_int = bool(value) if value else None
 | 
			
		||||
        elif self.field.type == CF_TYPE_DATE:
 | 
			
		||||
            self.val_date = value
 | 
			
		||||
        elif self.field.type == CF_TYPE_SELECT:
 | 
			
		||||
            self.val_int = value.id
 | 
			
		||||
        else:
 | 
			
		||||
            self.val_char = value
 | 
			
		||||
 | 
			
		||||
@@ -195,7 +218,7 @@ class ExportTemplate(models.Model):
 | 
			
		||||
class TopologyMap(models.Model):
 | 
			
		||||
    name = models.CharField(max_length=50, unique=True)
 | 
			
		||||
    slug = models.SlugField(unique=True)
 | 
			
		||||
    site = models.ForeignKey(Site, related_name='topology_maps', blank=True, null=True)
 | 
			
		||||
    site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True)
 | 
			
		||||
    device_patterns = models.TextField(help_text="Identify devices to include in the diagram using regular expressions,"
 | 
			
		||||
                                                 "one per line. Each line will result in a new tier of the drawing. "
 | 
			
		||||
                                                 "Separate multiple regexes on a line using commas. Devices will be "
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user