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

Closes #12129: Enable automatic synchronization of objects when DataFiles are updated (#12262)

* Closes #12129: Enable automatic synchronization of objects when DataFiles are updated

* Cleanup
This commit is contained in:
Jeremy Stretch
2023-04-17 10:35:17 -04:00
committed by GitHub
parent d470848b29
commit 8b040ff930
11 changed files with 120 additions and 17 deletions

View File

@ -80,12 +80,12 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
fieldsets = (
('File Upload', ('upload_file',)),
('Data Source', ('data_source', 'data_file')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
)
class Meta:
model = ManagedFile
fields = ('data_source', 'data_file')
fields = ('data_source', 'data_file', 'auto_sync_enabled')
def clean(self):
super().clean()

View File

@ -63,4 +63,21 @@ class Migration(migrations.Migration):
model_name='datafile',
index=models.Index(fields=['source', 'path'], name='core_datafile_source_path'),
),
migrations.CreateModel(
name='AutoSyncRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('object_id', models.PositiveBigIntegerField()),
('datafile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile')),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
],
),
migrations.AddIndex(
model_name='autosyncrecord',
index=models.Index(fields=['object_type', 'object_id'], name='core_autosy_object__c17bac_idx'),
),
migrations.AddConstraint(
model_name='autosyncrecord',
constraint=models.UniqueConstraint(fields=('object_type', 'object_id'), name='core_autosyncrecord_object'),
),
]

View File

@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('file_path', models.FilePathField(editable=False)),
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
('auto_sync_enabled', models.BooleanField(default=False)),
],
options={
'ordering': ('file_root', 'file_path'),

View File

@ -5,7 +5,8 @@ from fnmatch import fnmatchcase
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
@ -25,6 +26,7 @@ from ..signals import post_sync, pre_sync
from .jobs import Job
__all__ = (
'AutoSyncRecord',
'DataFile',
'DataSource',
)
@ -327,3 +329,35 @@ class DataFile(models.Model):
with open(path, 'wb+') as new_file:
new_file.write(self.data)
class AutoSyncRecord(models.Model):
"""
Maps a DataFile to a synced object for efficient automatic updating.
"""
datafile = models.ForeignKey(
to=DataFile,
on_delete=models.CASCADE,
related_name='+'
)
object_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
related_name='+'
)
object_id = models.PositiveBigIntegerField()
object = GenericForeignKey(
ct_field='object_type',
fk_field='object_id'
)
class Meta:
constraints = (
models.UniqueConstraint(
fields=('object_type', 'object_id'),
name='%(app_label)s_%(class)s_object'
),
)
indexes = (
models.Index(fields=('object_type', 'object_id')),
)

View File

@ -1,4 +1,4 @@
import django.dispatch
from django.dispatch import Signal, receiver
__all__ = (
'post_sync',
@ -6,5 +6,16 @@ __all__ = (
)
# DataSource signals
pre_sync = django.dispatch.Signal()
post_sync = django.dispatch.Signal()
pre_sync = Signal()
post_sync = Signal()
@receiver(post_sync)
def auto_sync(instance, **kwargs):
"""
Automatically synchronize any DataFiles with AutoSyncRecords after synchronizing a DataSource.
"""
from .models import AutoSyncRecord
for autosync in AutoSyncRecord.objects.filter(datafile__source=instance).prefetch_related('object'):
autosync.object.sync(save=True)

View File

@ -112,7 +112,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
fieldsets = (
('Export Template', ('name', 'content_types', 'description', 'template_code')),
('Data Source', ('data_source', 'data_file')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
)
@ -271,7 +271,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
fieldsets = (
('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
('Data Source', ('data_source', 'data_file')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
('Assignment', (
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
@ -283,7 +283,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
fields = (
'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
'tenants', 'tags', 'data_source', 'data_file',
'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled',
)
def __init__(self, *args, initial=None, **kwargs):
@ -322,7 +322,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
fieldsets = (
('Config Template', ('name', 'description', 'environment_params', 'tags')),
('Content', ('template_code',)),
('Data Source', ('data_source', 'data_file')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
)
class Meta:

View File

@ -26,6 +26,11 @@ class Migration(migrations.Migration):
name='data_source',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'),
),
migrations.AddField(
model_name='configcontext',
name='auto_sync_enabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='configcontext',
name='data_synced',
@ -47,6 +52,11 @@ class Migration(migrations.Migration):
name='data_source',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource'),
),
migrations.AddField(
model_name='exporttemplate',
name='auto_sync_enabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='exporttemplate',
name='data_synced',

View File

@ -25,6 +25,7 @@ class Migration(migrations.Migration):
('environment_params', models.JSONField(blank=True, default=dict, null=True)),
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
('auto_sync_enabled', models.BooleanField(default=False)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={

View File

@ -23,8 +23,7 @@ class SyncedDataMixin:
obj = get_object_or_404(self.queryset, pk=pk)
if obj.data_file:
obj.sync()
obj.save()
obj.sync(save=True)
serializer = self.serializer_class(obj, context={'request': request})
return Response(serializer.data)

View File

@ -3,6 +3,7 @@ from collections import defaultdict
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.db import models
from django.db.models.signals import class_prepared
@ -382,6 +383,10 @@ class SyncedDataMixin(models.Model):
editable=False,
help_text=_("Path to remote file (relative to data source root)")
)
auto_sync_enabled = models.BooleanField(
default=False,
help_text=_("Enable automatic synchronization of data when the data file is updated")
)
data_synced = models.DateTimeField(
blank=True,
null=True,
@ -404,10 +409,33 @@ class SyncedDataMixin(models.Model):
else:
self.data_source = None
self.data_path = ''
self.auto_sync_enabled = False
self.data_synced = None
super().clean()
def save(self, *args, **kwargs):
from core.models import AutoSyncRecord
ret = super().save(*args, **kwargs)
# Create/delete AutoSyncRecord as needed
content_type = ContentType.objects.get_for_model(self)
if self.auto_sync_enabled:
AutoSyncRecord.objects.get_or_create(
datafile=self.data_file,
object_type=content_type,
object_id=self.pk
)
else:
AutoSyncRecord.objects.filter(
datafile=self.data_file,
object_type=content_type,
object_id=self.pk
).delete()
return ret
def resolve_data_file(self):
"""
Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if
@ -421,13 +449,17 @@ class SyncedDataMixin(models.Model):
except DataFile.DoesNotExist:
pass
def sync(self):
def sync(self, save=False):
"""
Synchronize the object from it's assigned DataFile (if any). This wraps sync_data() and updates
the synced_data timestamp.
:param save: If true, save() will be called after data has been synchronized
"""
self.sync_data()
self.data_synced = timezone.now()
if save:
self.save()
def sync_data(self):
"""

View File

@ -205,8 +205,7 @@ class ObjectSyncDataView(View):
messages.error(request, f"Unable to synchronize data: No data file set.")
return redirect(obj.get_absolute_url())
obj.sync()
obj.save()
obj.sync(save=True)
messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.")
return redirect(obj.get_absolute_url())
@ -227,8 +226,7 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView):
with transaction.atomic():
for obj in selected_objects:
obj.sync()
obj.save()
obj.sync(save=True)
model_name = self.queryset.model._meta.verbose_name_plural
messages.success(request, f"Synced {len(selected_objects)} {model_name}")