mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Add contact assignments to models
This commit is contained in:
parent
2e78568d4d
commit
f193f0d3f9
@ -62,6 +62,11 @@ class Provider(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
@ -203,6 +208,11 @@ class Circuit(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
@ -54,6 +54,11 @@ class Manufacturer(OrganizationalModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
@ -584,6 +589,11 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
@ -40,6 +40,11 @@ class PowerPanel(PrimaryModel):
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
@ -175,12 +175,17 @@ class Rack(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='rack'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
@ -52,12 +52,17 @@ class Region(NestedGroupModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='region'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:region', args=[self.pk])
|
||||
@ -100,12 +105,17 @@ class SiteGroup(NestedGroupModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='site_group'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:sitegroup', args=[self.pk])
|
||||
@ -221,12 +231,17 @@ class Site(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='site'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
@ -291,12 +306,17 @@ class Location(NestedGroupModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='location'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
@ -70,11 +70,12 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||
{% include 'inc/image_attachments_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% include 'inc/image_attachments_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
|
@ -47,12 +47,13 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
|
||||
{% include 'inc/comments_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
|
@ -296,6 +296,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% include 'inc/image_attachments_panel.html' %}
|
||||
<div class="card noprint">
|
||||
<h5 class="card-header">
|
||||
|
@ -72,6 +72,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% include 'inc/image_attachments_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -38,6 +38,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,6 +44,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% include 'inc/image_attachments_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
@ -332,6 +332,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,6 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
@ -76,6 +76,10 @@
|
||||
<th scope="row">Facility</th>
|
||||
<td>{{ object.facility|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">AS Number</th>
|
||||
<td>{{ object.asn|placeholder }}</td>
|
||||
@ -91,19 +95,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Contact Info
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Physical Address</th>
|
||||
<td>
|
||||
@ -138,33 +129,57 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contact Name</th>
|
||||
<td>{{ object.contact_name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contact Phone</th>
|
||||
<td>
|
||||
{% if object.contact_phone %}
|
||||
<a href="tel:{{ object.contact_phone }}">{{ object.contact_phone }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contact E-Mail</th>
|
||||
<td>
|
||||
{% if object.contact_email %}
|
||||
<a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Contact Info</h5>
|
||||
<div class="card-body">
|
||||
{% with deprecation_warning="This field will be removed in a future release. Please migrate this data to contact objects." %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Contact Name</th>
|
||||
<td>
|
||||
{% if object.contact_name %}
|
||||
<div class="float-end text-warning">
|
||||
<i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ object.contact_name|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contact Phone</th>
|
||||
<td>
|
||||
{% if object.contact_phone %}
|
||||
<div class="float-end text-warning">
|
||||
<i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
|
||||
</div>
|
||||
<a href="tel:{{ object.contact_phone }}">{{ object.contact_phone }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Contact E-Mail</th>
|
||||
<td>
|
||||
{% if object.contact_email %}
|
||||
<div class="float-end text-warning">
|
||||
<i class="mdi mdi-alert" title="{{ deprecation_warning }}"></i>
|
||||
</div>
|
||||
<a href="tel:{{ object.contact_
|
||||
<a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %}
|
||||
{% include 'inc/comments_panel.html' %}
|
||||
|
@ -46,6 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
49
netbox/templates/inc/contacts_panel.html
Normal file
49
netbox/templates/inc/contacts_panel.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="card">
|
||||
<h5 class="card-header">Contacts</h5>
|
||||
<div class="card-body">
|
||||
{% with contacts=object.contacts.all %}
|
||||
{% if contacts.exists %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Priority</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for contact in contacts %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ contact.contact.get_absolute_url }}">{{ contact.contact }}</a>
|
||||
</td>
|
||||
<td>{{ contact.role|placeholder }}</td>
|
||||
<td>{{ contact.get_priority_display|placeholder }}</td>
|
||||
<td class="text-end noprint">
|
||||
{% if perms.tenancy.change_contactassignment %}
|
||||
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.tenancy.delete_contactassignment %}
|
||||
<a href="{% url 'extras:imageattachment_delete' pk=contact.pk %}" class="btn btn-danger btn-sm lh-1" title="Delete">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-muted">None</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% if perms.tenancy.add_contactassignment %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -38,6 +38,7 @@
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %}
|
||||
{% include 'inc/comments_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-5">
|
||||
|
@ -62,6 +62,7 @@
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,6 +32,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -173,6 +173,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/contacts_panel.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,15 @@
|
||||
from django import forms
|
||||
|
||||
from extras.forms import CustomFieldModelForm
|
||||
from extras.models import Tag
|
||||
from tenancy.models import *
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea,
|
||||
StaticSelect,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'ContactAssignmentForm',
|
||||
'ContactForm',
|
||||
'ContactGroupForm',
|
||||
'ContactRoleForm',
|
||||
@ -100,3 +104,25 @@ class ContactForm(BootstrapMixin, CustomFieldModelForm):
|
||||
widgets = {
|
||||
'address': SmallTextarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
contact = DynamicModelChoiceField(
|
||||
queryset=Contact.objects.all()
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=ContactRole.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
fields = (
|
||||
'group', 'contact', 'role', 'priority',
|
||||
)
|
||||
widgets = {
|
||||
'priority': StaticSelect(),
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@ -86,6 +86,11 @@ class Tenant(PrimaryModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
|
@ -67,4 +67,9 @@ urlpatterns = [
|
||||
path('contacts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='contact_changelog', kwargs={'model': Contact}),
|
||||
path('contacts/<int:pk>/journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}),
|
||||
|
||||
# Contact assignments
|
||||
path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'),
|
||||
path('contact-assignments/<int:pk>/edit/', views.ContactAssignmentEditView.as_view(), name='contactassignment_edit'),
|
||||
path('contact-assignments/<int:pk>/delete/', views.ContactAssignmentDeleteView.as_view(), name='contactassignment_delete'),
|
||||
|
||||
]
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Site, Rack, Device, RackReservation
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from netbox.views import generic
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
@ -309,3 +312,33 @@ class ContactBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Contact.objects.prefetch_related('group')
|
||||
filterset = filtersets.ContactFilterSet
|
||||
table = tables.ContactTable
|
||||
|
||||
|
||||
#
|
||||
# Contact assignments
|
||||
#
|
||||
|
||||
class ContactAssignmentEditView(generic.ObjectEditView):
|
||||
queryset = ContactAssignment.objects.all()
|
||||
model_form = forms.ContactAssignmentForm
|
||||
|
||||
def alter_obj(self, instance, request, args, kwargs):
|
||||
if not instance.pk:
|
||||
# Assign the object based on URL kwargs
|
||||
try:
|
||||
app_label, model = request.GET.get('content_type').split('.')
|
||||
except (AttributeError, ValueError):
|
||||
raise Http404("Content type not specified")
|
||||
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
|
||||
instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
||||
return instance
|
||||
|
||||
def get_return_url(self, request, obj=None):
|
||||
return obj.object.get_absolute_url() if obj else super().get_return_url(request)
|
||||
|
||||
|
||||
class ContactAssignmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ContactAssignment.objects.all()
|
||||
|
||||
def get_return_url(self, request, obj=None):
|
||||
return obj.object.get_absolute_url() if obj else super().get_return_url(request)
|
||||
|
@ -81,12 +81,17 @@ class ClusterGroup(OrganizationalModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='cluster_group'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@ -142,12 +147,17 @@ class Cluster(PrimaryModel):
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
to='ipam.VLANGroup',
|
||||
content_type_field='scope_type',
|
||||
object_id_field='scope_id',
|
||||
related_query_name='cluster'
|
||||
)
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@ -268,6 +278,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relation
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
objects = ConfigContextModelQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
|
Loading…
Reference in New Issue
Block a user