1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on custom fields

This commit is contained in:
Jeremy Stretch
2016-08-12 17:20:01 -04:00
parent bf1b8ab9b8
commit 550a05487d
8 changed files with 248 additions and 2 deletions

View File

@ -3,6 +3,7 @@ import re
from django import forms from django import forms
from django.db.models import Count, Q from django.db.models import Count, Q
from extras.forms import CustomFieldForm
from ipam.models import IPAddress from ipam.models import IPAddress
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
@ -78,7 +79,7 @@ def bulkedit_rackrole_choices():
# Sites # Sites
# #
class SiteForm(forms.ModelForm, BootstrapMixin): class SiteForm(BootstrapMixin, CustomFieldForm):
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()

View File

@ -1,6 +1,16 @@
from django.contrib import admin from django.contrib import admin
from .models import Graph, ExportTemplate, TopologyMap, UserAction from .models import CustomField, CustomFieldValue, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction
class CustomFieldChoiceAdmin(admin.TabularInline):
model = CustomFieldChoice
@admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin]
list_display = ['name', 'type', 'required', 'default', 'description']
@admin.register(Graph) @admin.register(Graph)

52
netbox/extras/forms.py Normal file
View File

@ -0,0 +1,52 @@
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
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))
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)
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
if cf.required:
field = forms.BooleanField(required=False)
else:
field = forms.NullBooleanField(required=False)
# Date
elif cf.type == CF_TYPE_DATE:
field = forms.DateField(blank=not cf.required)
# Select
elif cf.type == CF_TYPE_SELECT:
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
# Text
else:
field = forms.CharField(max_length=100, blank=not cf.required)
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)

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-12 19:52
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CustomField',
fields=[
('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)),
('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')),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='CustomFieldChoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(max_length=100)),
('weight', models.PositiveSmallIntegerField(default=100)),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')),
],
options={
'ordering': ['field', 'weight', 'value'],
},
),
migrations.CreateModel(
name='CustomFieldValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('obj_id', models.PositiveIntegerField()),
('val_int', models.BigIntegerField(blank=True, null=True)),
('val_char', models.CharField(blank=True, max_length=100)),
('val_date', models.DateField(blank=True, null=True)),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')),
('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
],
options={
'ordering': ['obj_type', 'obj_id'],
},
),
migrations.AlterUniqueTogether(
name='customfieldchoice',
unique_together=set([('field', 'value')]),
),
]

View File

@ -1,5 +1,7 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
@ -8,6 +10,26 @@ from django.utils.safestring import mark_safe
from dcim.models import Site from dcim.models import Site
CUSTOMFIELD_MODELS = (
'site', 'rack', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_SELECT = 500
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_SELECT, 'Selection'),
)
GRAPH_TYPE_INTERFACE = 100 GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200 GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300 GRAPH_TYPE_SITE = 300
@ -40,6 +62,80 @@ ACTION_CHOICES = (
) )
class CustomField(models.Model):
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS})
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")
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")
class Meta:
ordering = ['name']
def __unicode__(self):
return self.label or self.name
class CustomFieldValue(models.Model):
field = models.ForeignKey('CustomField', related_name='values')
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
obj_id = models.PositiveIntegerField()
obj = GenericForeignKey('obj_type', 'obj_id')
val_int = models.BigIntegerField(blank=True, null=True)
val_char = models.CharField(max_length=100, blank=True)
val_date = models.DateField(blank=True, null=True)
class Meta:
ordering = ['obj_type', 'obj_id']
def __unicode__(self):
return self.value
@property
def value(self):
if self.field.type == CF_TYPE_INTEGER:
return self.val_int
if self.field.type == CF_TYPE_BOOLEAN:
return bool(self.val_int) if self.val_int is not None else None
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 self.val_char
@value.setter
def value(self, value):
if self.field.type in [CF_TYPE_INTEGER, CF_TYPE_SELECT]:
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
else:
self.val_char = value
class CustomFieldChoice(models.Model):
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
on_delete=models.CASCADE)
value = models.CharField(max_length=100)
weight = models.PositiveSmallIntegerField(default=100)
class Meta:
ordering = ['field', 'weight', 'value']
unique_together = ['field', 'value']
def __unicode__(self):
return self.value
def clean(self):
if self.field.type != CF_TYPE_SELECT:
raise ValidationError("Custom field choices can only be assigned to selection fields.")
class Graph(models.Model): class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
weight = models.PositiveSmallIntegerField(default=1000) weight = models.PositiveSmallIntegerField(default=1000)

View File

@ -14,6 +14,12 @@
{% render_field form.shipping_address %} {% render_field form.shipping_address %}
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -0,0 +1,7 @@
{% load form_helpers %}
{% for field in form %}
{% if field.name in form.custom_fields %}
{% render_field field %}
{% endif %}
{% endfor %}

View File

@ -14,6 +14,16 @@ def render_field(field):
} }
@register.inclusion_tag('utilities/render_custom_fields.html')
def render_custom_fields(form):
"""
Render all custom fields in a form
"""
return {
'form': form,
}
@register.inclusion_tag('utilities/render_form.html') @register.inclusion_tag('utilities/render_form.html')
def render_form(form): def render_form(form):
""" """