Skip to content

Commit

Permalink
[change/fix] Improved device export #850
Browse files Browse the repository at this point in the history
- Added more device, config and geo fields to export
- Fixes #850
  • Loading branch information
nemesifier committed Apr 9, 2024
1 parent 2c58297 commit 32c712a
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 37 deletions.
24 changes: 1 addition & 23 deletions openwisp_controller/config/admin.py
Expand Up @@ -21,7 +21,6 @@
from django.urls import path, re_path, reverse
from django.utils.translation import gettext_lazy as _
from flat_json_widget.widgets import FlatJsonWidget
from import_export import resources
from import_export.admin import ImportExportMixin
from openwisp_ipam.filters import SubnetFilter
from swapper import load_model
Expand All @@ -38,6 +37,7 @@
from ..admin import MultitenantAdminMixin
from . import settings as app_settings
from .base.vpn import AbstractVpn
from .exportable import DeviceResource
from .filters import DeviceGroupFilter, GroupFilter, TemplatesFilter
from .utils import send_file
from .widgets import DeviceGroupJsonSchemaWidget, JsonSchemaWidget
Expand Down Expand Up @@ -742,28 +742,6 @@ def add_reversion_following(cls, follow):
)


class DeviceResource(resources.ModelResource):
class Meta:
model = Device
fields = [
'name',
'mac_address',
'organization__name',
'group__name',
'config__status',
'config__backend',
'last_ip',
'management_ip',
'created',
'modified',
'id',
'key',
'organization',
'group',
]
export_order = fields


class DeviceAdminExportable(ImportExportMixin, DeviceAdmin):
resource_class = DeviceResource
# needed to support both reversion and import-export
Expand Down
68 changes: 68 additions & 0 deletions openwisp_controller/config/exportable.py
@@ -0,0 +1,68 @@
import json

from django.core.exceptions import ObjectDoesNotExist
from import_export import resources, widgets
from import_export.fields import Field
from swapper import load_model

from . import settings as app_settings

Device = load_model('config', 'Device')
Config = load_model('config', 'Config')


class DeviceResource(resources.ModelResource):
organization = Field(attribute='organization__name', column_name='organization')
group = Field(attribute='group__name', column_name='group')
config_status = Field(attribute='config__status', column_name='config_status')
config_backend = Field(attribute='config__backend', column_name='config_backend')
config_data = Field(attribute='config__config', column_name='config_data')
config_context = Field(attribute='config__context', column_name='config_context')
config_templates = Field(
attribute='config__templates',
column_name='config_templates',
widget=widgets.ManyToManyWidget(Config, field='pk', separator=','),
)
organization_id = Field(attribute='organization_id', column_name='organization_id')
group_id = Field(attribute='group_id', column_name='group_id')

def dehydrate_config_data(self, device):
try:
return json.dumps(device.config.config, sort_keys=True)
except ObjectDoesNotExist:
pass

def dehydrate_config_context(self, device):
try:
return json.dumps(device.config.context, sort_keys=True)
except ObjectDoesNotExist:
pass

class Meta:
model = Device
fields = [
'name',
'mac_address',
'organization',
'group',
'model',
'os',
'system',
'notes',
'last_ip',
'management_ip',
'config_status',
'config_backend',
'config_data',
'config_context',
'config_templates',
'created',
'modified',
'id',
'key',
'organization_id',
'group_id',
]
if app_settings.HARDWARE_ID_ENABLED:
fields.insert(1, 'hardware_id')
export_order = fields
23 changes: 15 additions & 8 deletions openwisp_controller/config/tests/test_admin.py
Expand Up @@ -55,18 +55,24 @@ class TestImportExportMixin:
resource_fields = [
'name',
'mac_address',
'organization__name',
'group__name',
'config__status',
'config__backend',
'organization',
'group',
'model',
'os',
'system',
'last_ip',
'management_ip',
'config_status',
'config_backend',
'config_data',
'config_context',
'config_templates',
'created',
'modified',
'key',
'id',
'organization',
'group',
'organization_id',
'group_id',
]

def test_device_import_export_buttons(self):
Expand All @@ -90,7 +96,8 @@ def test_device_export(self):
def test_device_import(self):
org = self._get_org()
contents = (
'organization,name,mac_address\n' f'{org.pk},TestImport,00:11:22:09:44:55'
'organization_id,name,mac_address\n'
f'{org.pk},TestImport,00:11:22:09:44:55'
)
csv = ContentFile(contents)
response = self.client.post(
Expand Down Expand Up @@ -487,7 +494,7 @@ def test_device_import_with_group_apply_templates(self):
dg = self._create_device_group(name='test-group', organization=org)
dg.templates.add(template)
contents = (
'organization,name,mac_address,group\n'
'organization_id,name,mac_address,group_id\n'
f'{org.pk},TestImport,00:11:22:09:44:55,{dg.pk}'
)
csv = ContentFile(contents)
Expand Down
12 changes: 7 additions & 5 deletions openwisp_controller/geo/admin.py
Expand Up @@ -15,7 +15,8 @@
from openwisp_users.multitenancy import MultitenantOrgFilter

from ..admin import MultitenantAdminMixin
from ..config.admin import DeviceAdmin
from ..config.admin import DeviceAdminExportable
from .exportable import GeoDeviceResource

DeviceLocation = load_model('geo', 'DeviceLocation')
FloorPlan = load_model('geo', 'FloorPlan')
Expand Down Expand Up @@ -98,8 +99,9 @@ def queryset(self, request, queryset):
return queryset


# Prepend DeviceLocationInline to config.DeviceAdmin
DeviceAdmin.inlines.insert(1, DeviceLocationInline)
DeviceAdmin.list_filter.append(DeviceLocationFilter)
# Prepend DeviceLocationInline to config.DeviceAdminExportable
DeviceAdminExportable.inlines.insert(1, DeviceLocationInline)
DeviceAdminExportable.list_filter.append(DeviceLocationFilter)
DeviceAdminExportable.resource_class = GeoDeviceResource
reversion.register(model=DeviceLocation, follow=['device'])
DeviceAdmin.add_reversion_following(follow=['devicelocation'])
DeviceAdminExportable.add_reversion_following(follow=['devicelocation'])
52 changes: 52 additions & 0 deletions openwisp_controller/geo/exportable.py
@@ -0,0 +1,52 @@
from django.core.exceptions import ObjectDoesNotExist
from import_export.fields import Field

from ..config.exportable import DeviceResource


class GeoDeviceResource(DeviceResource):
venue = Field(attribute='devicelocation__location__name', column_name='venue')
address = Field(
attribute='devicelocation__location__address', column_name='address'
)
coords = Field(attribute='devicelocation__location__geometry', column_name='coords')
is_mobile = Field(
attribute='devicelocation__location__is_mobile', column_name='is_mobile'
)
venue_type = Field(
attribute='devicelocation__location__type', column_name='venue_type'
)
floor = Field(attribute='devicelocation__floorplan__floor', column_name='floor')
floor_position = Field(
attribute='devicelocation__indoor', column_name='floor_position'
)
location_id = Field(
attribute='devicelocation__location_id', column_name='location_id'
)
floorplan_id = Field(
attribute='devicelocation__floorplan_id', column_name='floorplan_id'
)

def dehydrate_coords(self, device):
try:
return device.devicelocation.location.geometry.wkt
except ObjectDoesNotExist:
pass

class Meta(DeviceResource.Meta):
fields = DeviceResource.Meta.fields[:] # copy
# add geo fields after before last_ip
# fmt: off
fields[fields.index('last_ip'):fields.index('last_ip')] = [
'venue',
'address',
'coords',
'is_mobile',
'venue_type',
'floor',
'floor_position',
]
# fmt: on
# add id fields at the end
fields += ['location_id', 'floorplan_id']
export_order = fields
30 changes: 30 additions & 0 deletions openwisp_controller/geo/tests/test_admin.py
Expand Up @@ -4,6 +4,9 @@
from django_loci.tests.base.test_admin import BaseTestAdmin
from swapper import load_model

from openwisp_users.tests.utils import TestOrganizationMixin

from ...config.tests.test_admin import TestImportExportMixin
from ...tests.utils import TestAdminMixin
from .utils import TestGeoMixin

Expand Down Expand Up @@ -132,3 +135,30 @@ def test_admin_menu_groups(self):
'<div class="mg-dropdown-label">Geographic Info </div>',
html=True,
)


class TestDeviceAdmin(
TestImportExportMixin, TestAdminMixin, TestGeoMixin, TestOrganizationMixin, TestCase
):
app_label = 'config'
fixtures = ['test_templates']
object_model = Device
location_model = Location
floorplan_model = FloorPlan
object_location_model = DeviceLocation
user_model = get_user_model()

resource_fields = TestImportExportMixin.resource_fields[:] + [
'venue',
'address',
'coords',
'is_mobile',
'venue_type',
'floor',
'floor_position',
'location_id',
'floorplan_id',
]

def setUp(self):
self.client.force_login(self._get_admin())
2 changes: 1 addition & 1 deletion requirements.txt
Expand Up @@ -15,4 +15,4 @@ scp~=0.14.2
django-cache-memoize~=0.1.0
shortuuid~=1.0.1
netaddr~=0.8.0
django-import-export~=3.2.0
django-import-export~=3.3.0

0 comments on commit 32c712a

Please sign in to comment.