mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Build out UI & API resources
This commit is contained in:
parent
a5822ade84
commit
41607f9a52
@ -1,18 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from core.models import DataFile, DataSource
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(DataSource)
|
|
||||||
class DataSourceAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'file_count', 'last_synced')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def file_count(obj):
|
|
||||||
return obj.datafiles.count()
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(DataFile)
|
|
||||||
class DataFileAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('path', 'size', 'last_updated')
|
|
||||||
readonly_fields = ('source', 'path', 'last_updated', 'size', 'hash')
|
|
0
netbox/core/api/__init__.py
Normal file
0
netbox/core/api/__init__.py
Normal file
25
netbox/core/api/nested_serializers.py
Normal file
25
netbox/core/api/nested_serializers.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import *
|
||||||
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'NestedDataFileSerializer',
|
||||||
|
'NestedDataSourceSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedDataFileSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataFile
|
||||||
|
fields = ['id', 'url', 'display', 'path']
|
37
netbox/core/api/serializers.py
Normal file
37
netbox/core/api/serializers.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.choices import *
|
||||||
|
from core.models import *
|
||||||
|
from netbox.api.fields import ChoiceField
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataSourceSerializer',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||||
|
type = ChoiceField(choices=DataSourceTypeChoices, required=False)
|
||||||
|
|
||||||
|
# Related object counts
|
||||||
|
file_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'type', 'url', 'enabled', 'description', 'git_branch', 'ignore_rules',
|
||||||
|
'username', 'password', 'created', 'last_updated', 'file_count',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||||
|
source = NestedDataSourceSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataFile
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||||
|
]
|
13
netbox/core/api/urls.py
Normal file
13
netbox/core/api/urls.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from netbox.api.routers import NetBoxRouter
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
router = NetBoxRouter()
|
||||||
|
router.APIRootView = views.CoreRootView
|
||||||
|
|
||||||
|
# Data sources
|
||||||
|
router.register('data-sources', views.DataSourceViewSet)
|
||||||
|
router.register('data-files', views.DataFileViewSet)
|
||||||
|
|
||||||
|
app_name = 'core-api'
|
||||||
|
urlpatterns = router.urls
|
33
netbox/core/api/views.py
Normal file
33
netbox/core/api/views.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
|
from core import filtersets
|
||||||
|
from core.models import *
|
||||||
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
|
from utilities.utils import count_related
|
||||||
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class CoreRootView(APIRootView):
|
||||||
|
"""
|
||||||
|
Core API root view
|
||||||
|
"""
|
||||||
|
def get_view_name(self):
|
||||||
|
return 'Core'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Data sources
|
||||||
|
#
|
||||||
|
|
||||||
|
class DataSourceViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = DataSource.objects.annotate(
|
||||||
|
file_count=count_related(DataFile, 'source')
|
||||||
|
)
|
||||||
|
serializer_class = serializers.DataSourceSerializer
|
||||||
|
filterset_class = filtersets.DataSourceFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = DataFile.objects.defer('data').prefetch_related('source')
|
||||||
|
serializer_class = serializers.DataFileSerializer
|
||||||
|
filterset_class = filtersets.DataFileFilterSet
|
50
netbox/core/filtersets.py
Normal file
50
netbox/core/filtersets.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataFileFilterSet',
|
||||||
|
'DataSourceFilterSet',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceFilterSet(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = ('id', 'name', 'type', 'git_branch', 'username')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileFilterSet(django_filters.FilterSet):
|
||||||
|
datasource_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
label=_('Data source (ID)'),
|
||||||
|
)
|
||||||
|
datasource = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='source__name',
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('Data source (name)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataFile
|
||||||
|
fields = ('id', 'path', 'last_updated', 'size', 'hash')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(path__icontains=value)
|
||||||
|
)
|
4
netbox/core/forms/__init__.py
Normal file
4
netbox/core/forms/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .bulk_edit import *
|
||||||
|
from .bulk_import import *
|
||||||
|
from .filtersets import *
|
||||||
|
from .model_forms import *
|
57
netbox/core/forms/bulk_edit.py
Normal file
57
netbox/core/forms/bulk_edit.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.choices import DataSourceTypeChoices
|
||||||
|
from core.models import *
|
||||||
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
|
from utilities.forms import (
|
||||||
|
add_blank_choice, BulkEditNullBooleanSelect, StaticSelect,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataSourceBulkEditForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(DataSourceTypeChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect(),
|
||||||
|
label=_('Enforce unique space')
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
git_branch = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
ignore_rules = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.Textarea()
|
||||||
|
)
|
||||||
|
username = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False,
|
||||||
|
widget=forms.PasswordInput()
|
||||||
|
)
|
||||||
|
|
||||||
|
model = DataSource
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('type', 'enabled', 'description', 'git_branch', 'ignore_rules')),
|
||||||
|
('Authentication', ('username', 'password')),
|
||||||
|
)
|
||||||
|
nullable_fields = (
|
||||||
|
'description', 'description', 'git_branch', 'ignore_rules', 'username', 'password',
|
||||||
|
)
|
15
netbox/core/forms/bulk_import.py
Normal file
15
netbox/core/forms/bulk_import.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from core.models import *
|
||||||
|
from netbox.forms import NetBoxModelImportForm
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataSourceImportForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceImportForm(NetBoxModelImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = (
|
||||||
|
'name', 'type', 'url', 'enabled', 'description', 'git_branch', 'ignore_rules', 'username', 'password',
|
||||||
|
)
|
56
netbox/core/forms/filtersets.py
Normal file
56
netbox/core/forms/filtersets.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from core.choices import *
|
||||||
|
from core.models import *
|
||||||
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
|
from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataFileFilterForm',
|
||||||
|
'DataSourceFilterForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = DataSource
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id')),
|
||||||
|
('Data Source', ('type', 'git_branch')),
|
||||||
|
('Authentication', ('username',)),
|
||||||
|
)
|
||||||
|
type = MultipleChoiceField(
|
||||||
|
choices=DataSourceTypeChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
git_branch = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
username = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = DataFile
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id')),
|
||||||
|
('File', ('datasource_id',)),
|
||||||
|
)
|
||||||
|
datasource_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
type = MultipleChoiceField(
|
||||||
|
choices=DataSourceTypeChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
git_branch = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
username = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
23
netbox/core/forms/model_forms.py
Normal file
23
netbox/core/forms/model_forms.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from core.models import *
|
||||||
|
from netbox.forms import NetBoxModelForm, StaticSelect
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataSourceForm',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceForm(NetBoxModelForm):
|
||||||
|
fieldsets = (
|
||||||
|
('Source', ('name', 'type', 'url', 'enabled', 'description')),
|
||||||
|
('Git', ('git_branch',)),
|
||||||
|
('Authentication', ('username', 'password')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataSource
|
||||||
|
fields = [
|
||||||
|
'name', 'type', 'url', 'enabled', 'description', 'git_branch', 'ignore_rules', 'username', 'password',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'type': StaticSelect(),
|
||||||
|
}
|
0
netbox/core/graphql/__init__.py
Normal file
0
netbox/core/graphql/__init__.py
Normal file
12
netbox/core/graphql/schema.py
Normal file
12
netbox/core/graphql/schema.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
|
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||||
|
from .types import *
|
||||||
|
|
||||||
|
|
||||||
|
class CoreQuery(graphene.ObjectType):
|
||||||
|
datafile = ObjectField(DataFileType)
|
||||||
|
datafile_list = ObjectListField(DataFileType)
|
||||||
|
|
||||||
|
datasource = ObjectField(DataSourceType)
|
||||||
|
datasource_list = ObjectListField(DataSourceType)
|
21
netbox/core/graphql/types.py
Normal file
21
netbox/core/graphql/types.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from core import filtersets, models
|
||||||
|
from netbox.graphql.types import NetBoxObjectType
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataFileType',
|
||||||
|
'DataSourceType',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileType(NetBoxObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = models.DataFile
|
||||||
|
exclude = ('data',)
|
||||||
|
filterset_class = filtersets.DataFileFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceType(NetBoxObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = models.DataSource
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.DataSourceFilterSet
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-26 20:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_datasource_last_synced'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='datasource',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='datasource',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
1
netbox/core/models/__init__.py
Normal file
1
netbox/core/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .data import *
|
@ -2,18 +2,18 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from functools import cached_property
|
|
||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
from urllib.parse import quote, urlunparse, urlparse
|
from urllib.parse import quote, urlunparse, urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from netbox.models import ChangeLoggedModel
|
||||||
from utilities.files import sha256_hash
|
from utilities.files import sha256_hash
|
||||||
from .choices import *
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
from ..choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'DataSource',
|
'DataSource',
|
||||||
@ -23,7 +23,7 @@ __all__ = (
|
|||||||
logger = logging.getLogger('netbox.core.data')
|
logger = logging.getLogger('netbox.core.data')
|
||||||
|
|
||||||
|
|
||||||
class DataSource(models.Model):
|
class DataSource(ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A remote source from which DataFiles are synchronized.
|
A remote source from which DataFiles are synchronized.
|
||||||
"""
|
"""
|
||||||
@ -36,6 +36,10 @@ class DataSource(models.Model):
|
|||||||
choices=DataSourceTypeChoices,
|
choices=DataSourceTypeChoices,
|
||||||
default=DataSourceTypeChoices.LOCAL
|
default=DataSourceTypeChoices.LOCAL
|
||||||
)
|
)
|
||||||
|
url = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name=_('URL')
|
||||||
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
@ -43,9 +47,10 @@ class DataSource(models.Model):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
url = models.CharField(
|
git_branch = models.CharField(
|
||||||
max_length=200,
|
max_length=100,
|
||||||
verbose_name=_('URL')
|
blank=True,
|
||||||
|
help_text=_("Branch to check out for git sources (if not using the default)")
|
||||||
)
|
)
|
||||||
ignore_rules = models.TextField(
|
ignore_rules = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -59,10 +64,6 @@ class DataSource(models.Model):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
git_branch = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
last_synced = models.DateTimeField(
|
last_synced = models.DateTimeField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@ -75,8 +76,8 @@ class DataSource(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.name}'
|
return f'{self.name}'
|
||||||
|
|
||||||
# def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
# return reverse('core:datasource', args=[self.pk])
|
return reverse('core:datasource', args=[self.pk])
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
"""
|
"""
|
||||||
@ -231,6 +232,8 @@ class DataFile(models.Model):
|
|||||||
)
|
)
|
||||||
data = models.BinaryField()
|
data = models.BinaryField()
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('source', 'path')
|
ordering = ('source', 'path')
|
||||||
constraints = (
|
constraints = (
|
1
netbox/core/tables/__init__.py
Normal file
1
netbox/core/tables/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .data import *
|
43
netbox/core/tables/data.py
Normal file
43
netbox/core/tables/data.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from core.models import *
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DataFileTable',
|
||||||
|
'DataSourceTable',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceTable(NetBoxTable):
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
type = columns.ChoiceFieldColumn()
|
||||||
|
enabled = columns.BooleanColumn()
|
||||||
|
file_count = tables.Column(
|
||||||
|
verbose_name='Files'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = DataSource
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'type', 'enabled', 'url', 'description', 'git_branch', 'username', 'created',
|
||||||
|
'last_updated', 'file_count',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'type', 'enabled', 'description', 'file_count')
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileTable(NetBoxTable):
|
||||||
|
source = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
last_updated = columns.DateTimeColumn()
|
||||||
|
actions = None
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = DataFile
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'source', 'path', 'size', 'last_updated')
|
20
netbox/core/urls.py
Normal file
20
netbox/core/urls.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from utilities.urls import get_model_urls
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'core'
|
||||||
|
urlpatterns = (
|
||||||
|
|
||||||
|
# Data sources
|
||||||
|
path('data-sources/', views.DataSourceListView.as_view(), name='datasource_list'),
|
||||||
|
path('data-sources/add/', views.DataSourceEditView.as_view(), name='datasource_add'),
|
||||||
|
path('data-sources/import/', views.DataSourceBulkImportView.as_view(), name='datasource_import'),
|
||||||
|
path('data-sources/edit/', views.DataSourceBulkEditView.as_view(), name='datasource_bulk_edit'),
|
||||||
|
path('data-sources/delete/', views.DataSourceBulkDeleteView.as_view(), name='datasource_bulk_delete'),
|
||||||
|
path('data-sources/<int:pk>/', include(get_model_urls('core', 'datasource'))),
|
||||||
|
|
||||||
|
# Data files
|
||||||
|
path('data-files/', views.DataFileListView.as_view(), name='datafile_list'),
|
||||||
|
|
||||||
|
)
|
69
netbox/core/views.py
Normal file
69
netbox/core/views.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from netbox.views import generic
|
||||||
|
from utilities.utils import count_related
|
||||||
|
from utilities.views import register_model_view
|
||||||
|
from . import filtersets, forms, tables
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Data sources
|
||||||
|
#
|
||||||
|
|
||||||
|
class DataSourceListView(generic.ObjectListView):
|
||||||
|
queryset = DataSource.objects.annotate(
|
||||||
|
file_count=count_related(DataFile, 'source')
|
||||||
|
)
|
||||||
|
filterset = filtersets.DataSourceFilterSet
|
||||||
|
filterset_form = forms.DataSourceFilterForm
|
||||||
|
table = tables.DataSourceTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(DataSource)
|
||||||
|
class DataSourceView(generic.ObjectView):
|
||||||
|
queryset = DataSource.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(DataSource, 'edit')
|
||||||
|
class DataSourceEditView(generic.ObjectEditView):
|
||||||
|
queryset = DataSource.objects.all()
|
||||||
|
form = forms.DataSourceForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(DataSource, 'delete')
|
||||||
|
class DataSourceDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = DataSource.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = DataSource.objects.all()
|
||||||
|
model_form = forms.DataSourceImportForm
|
||||||
|
table = tables.DataSourceTable
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = DataSource.objects.annotate(
|
||||||
|
count_files=count_related(DataFile, 'source')
|
||||||
|
)
|
||||||
|
filterset = filtersets.DataSourceFilterSet
|
||||||
|
table = tables.DataSourceTable
|
||||||
|
form = forms.DataSourceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = DataSource.objects.annotate(
|
||||||
|
count_files=count_related(DataFile, 'source')
|
||||||
|
)
|
||||||
|
filterset = filtersets.DataSourceFilterSet
|
||||||
|
table = tables.DataSourceTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Data files
|
||||||
|
#
|
||||||
|
|
||||||
|
class DataFileListView(generic.ObjectListView):
|
||||||
|
queryset = DataFile.objects.defer('data')
|
||||||
|
filterset = filtersets.DataFileFilterSet
|
||||||
|
filterset_form = forms.DataFileFilterForm
|
||||||
|
table = tables.DataFileTable
|
||||||
|
actions = ('edit',)
|
@ -27,6 +27,7 @@ class APIRootView(APIView):
|
|||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'circuits': reverse('circuits-api:api-root', request=request, format=format),
|
'circuits': reverse('circuits-api:api-root', request=request, format=format),
|
||||||
|
'core': reverse('core-api:api-root', request=request, format=format),
|
||||||
'dcim': reverse('dcim-api:api-root', request=request, format=format),
|
'dcim': reverse('dcim-api:api-root', request=request, format=format),
|
||||||
'extras': reverse('extras-api:api-root', request=request, format=format),
|
'extras': reverse('extras-api:api-root', request=request, format=format),
|
||||||
'ipam': reverse('ipam-api:api-root', request=request, format=format),
|
'ipam': reverse('ipam-api:api-root', request=request, format=format),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from circuits.graphql.schema import CircuitsQuery
|
from circuits.graphql.schema import CircuitsQuery
|
||||||
|
from core.graphql.schema import CoreQuery
|
||||||
from dcim.graphql.schema import DCIMQuery
|
from dcim.graphql.schema import DCIMQuery
|
||||||
from extras.graphql.schema import ExtrasQuery
|
from extras.graphql.schema import ExtrasQuery
|
||||||
from ipam.graphql.schema import IPAMQuery
|
from ipam.graphql.schema import IPAMQuery
|
||||||
@ -14,6 +15,7 @@ from wireless.graphql.schema import WirelessQuery
|
|||||||
class Query(
|
class Query(
|
||||||
UsersQuery,
|
UsersQuery,
|
||||||
CircuitsQuery,
|
CircuitsQuery,
|
||||||
|
CoreQuery,
|
||||||
DCIMQuery,
|
DCIMQuery,
|
||||||
ExtrasQuery,
|
ExtrasQuery,
|
||||||
IPAMQuery,
|
IPAMQuery,
|
||||||
|
@ -287,6 +287,7 @@ OTHER_MENU = Menu(
|
|||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Integrations'),
|
label=_('Integrations'),
|
||||||
items=(
|
items=(
|
||||||
|
get_model_item('core', 'datasource', _('Data Sources')),
|
||||||
get_model_item('extras', 'webhook', _('Webhooks')),
|
get_model_item('extras', 'webhook', _('Webhooks')),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='extras:report_list',
|
link='extras:report_list',
|
||||||
|
@ -42,6 +42,7 @@ _patterns = [
|
|||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
path('circuits/', include('circuits.urls')),
|
path('circuits/', include('circuits.urls')),
|
||||||
|
path('core/', include('core.urls')),
|
||||||
path('dcim/', include('dcim.urls')),
|
path('dcim/', include('dcim.urls')),
|
||||||
path('extras/', include('extras.urls')),
|
path('extras/', include('extras.urls')),
|
||||||
path('ipam/', include('ipam.urls')),
|
path('ipam/', include('ipam.urls')),
|
||||||
@ -53,6 +54,7 @@ _patterns = [
|
|||||||
# API
|
# API
|
||||||
path('api/', APIRootView.as_view(), name='api-root'),
|
path('api/', APIRootView.as_view(), name='api-root'),
|
||||||
path('api/circuits/', include('circuits.api.urls')),
|
path('api/circuits/', include('circuits.api.urls')),
|
||||||
|
path('api/core/', include('core.api.urls')),
|
||||||
path('api/dcim/', include('dcim.api.urls')),
|
path('api/dcim/', include('dcim.api.urls')),
|
||||||
path('api/extras/', include('extras.api.urls')),
|
path('api/extras/', include('extras.api.urls')),
|
||||||
path('api/ipam/', include('ipam.api.urls')),
|
path('api/ipam/', include('ipam.api.urls')),
|
||||||
|
93
netbox/templates/core/datasource.html
Normal file
93
netbox/templates/core/datasource.html
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Data Source</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Type</th>
|
||||||
|
<td>{{ object.get_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enabled</th>
|
||||||
|
<td>{% checkmark object.enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">URL</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ object.url }}">{{ object.url }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Last synced</th>
|
||||||
|
<td>{{ object.last_synced|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Git branch</th>
|
||||||
|
<td>{{ object.git_branch|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Ignore rules</th>
|
||||||
|
<td>
|
||||||
|
{% if object.ignore_rules %}
|
||||||
|
<pre>{{ object.ignore_rules }}</pre>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">File count</th>
|
||||||
|
<td>{{ object.datafiles.count }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Authentication</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Username</th>
|
||||||
|
<td>{{ object.username|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Password</th>
|
||||||
|
<td>{% if object.password %}********{% else %}{{ ''|placeholder }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Files</h5>
|
||||||
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
hx-get="{% url 'core:datafile_list' %}?source_id={{ object.pk }}"
|
||||||
|
hx-trigger="load"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user