mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-18 15:34:36 +01:00
Merge branch 'main' into feature
This commit is contained in:
@@ -29,6 +29,6 @@ class ConfigTemplateSerializer(
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code',
|
||||
'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file',
|
||||
'data_synced', 'owner', 'tags', 'created', 'last_updated',
|
||||
'auto_sync_enabled', 'data_synced', 'owner', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@@ -276,6 +276,14 @@ class ScriptViewSet(ModelViewSet):
|
||||
_ignore_model_permissions = True
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
# Restrict the view's QuerySet to allow only the permitted objects
|
||||
if request.user.is_authenticated:
|
||||
action = 'run' if request.method == 'POST' else 'view'
|
||||
self.queryset = self.queryset.restrict(request.user, action)
|
||||
|
||||
def _get_script(self, pk):
|
||||
# If pk is numeric, retrieve script by ID
|
||||
if pk.isnumeric():
|
||||
@@ -299,10 +307,12 @@ class ScriptViewSet(ModelViewSet):
|
||||
"""
|
||||
Run a Script identified by its numeric PK or module & name and return the pending Job as the result
|
||||
"""
|
||||
if not request.user.has_perm('extras.run_script'):
|
||||
raise PermissionDenied("This user does not have permission to run scripts.")
|
||||
|
||||
script = self._get_script(pk)
|
||||
|
||||
if not request.user.has_perm('extras.run_script', obj=script):
|
||||
raise PermissionDenied("This user does not have permission to run this script.")
|
||||
|
||||
input_serializer = serializers.ScriptInputSerializer(
|
||||
data=request.data,
|
||||
context={'script': script}
|
||||
|
||||
@@ -209,7 +209,10 @@ class ObjectCountsWidget(DashboardWidget):
|
||||
url = get_action_url(model, action='list')
|
||||
except NoReverseMatch:
|
||||
url = None
|
||||
qs = model.objects.restrict(request.user, 'view')
|
||||
try:
|
||||
qs = model.objects.restrict(request.user, 'view')
|
||||
except AttributeError:
|
||||
qs = model.objects.all()
|
||||
# Apply any specified filters
|
||||
if url and (filters := self.config.get('filters')):
|
||||
params = dict_to_querydict(filters)
|
||||
|
||||
@@ -134,11 +134,18 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
|
||||
# Enqueue a Job to record the script's execution
|
||||
from extras.jobs import ScriptJob
|
||||
params = {
|
||||
"instance": event_rule.action_object,
|
||||
"name": script.name,
|
||||
"user": user,
|
||||
"data": event_data
|
||||
}
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
params["request"] = copy_safe_request(request)
|
||||
ScriptJob.enqueue(
|
||||
instance=event_rule.action_object,
|
||||
name=script.name,
|
||||
user=user,
|
||||
data=event_data
|
||||
**params
|
||||
)
|
||||
|
||||
# Notification groups
|
||||
|
||||
@@ -392,8 +392,12 @@ class ConfigTemplateBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
|
||||
nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension')
|
||||
auto_sync_enabled = forms.NullBooleanField(
|
||||
label=_('Auto sync enabled'),
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension', 'auto_sync_enabled',)
|
||||
|
||||
|
||||
class ImageAttachmentBulkEditForm(ChangelogMessageMixin, BulkEditForm):
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectType
|
||||
from core.models import DataFile, DataSource, ObjectType
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from netbox.events import get_event_type_choices
|
||||
@@ -160,14 +160,41 @@ class ConfigContextProfileImportForm(PrimaryModelImportForm):
|
||||
|
||||
|
||||
class ConfigTemplateImportForm(OwnerCSVMixin, CSVModelForm):
|
||||
data_source = CSVModelChoiceField(
|
||||
label=_('Data source'),
|
||||
queryset=DataSource.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Data source which provides the data file')
|
||||
)
|
||||
data_file = CSVModelChoiceField(
|
||||
label=_('Data file'),
|
||||
queryset=DataFile.objects.all(),
|
||||
required=False,
|
||||
to_field_name='path',
|
||||
help_text=_('Data file containing the template code')
|
||||
)
|
||||
auto_sync_enabled = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Auto sync enabled'),
|
||||
help_text=_("Enable automatic synchronization of template content when the data file is updated")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigTemplate
|
||||
fields = (
|
||||
'name', 'description', 'template_code', 'environment_params', 'mime_type', 'file_name', 'file_extension',
|
||||
'as_attachment', 'owner', 'tags',
|
||||
'name', 'description', 'template_code', 'data_source', 'data_file', 'auto_sync_enabled',
|
||||
'environment_params', 'mime_type', 'file_name', 'file_extension', 'as_attachment', 'owner', 'tags',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Make sure template_code is None when it's not included in the uploaded data
|
||||
if not self.data.get('template_code') and not self.data.get('data_file'):
|
||||
raise forms.ValidationError(_("Must specify either local content or a data file"))
|
||||
return self.cleaned_data['template_code']
|
||||
|
||||
|
||||
class SavedFilterImportForm(OwnerCSVMixin, CSVModelForm):
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
|
||||
@@ -43,17 +43,20 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = CustomField
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet(
|
||||
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'unique', 'choice_set_id',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('object_type_id', 'type', 'group_name', 'weight', 'required', 'unique', name=_('Attributes')),
|
||||
FieldSet('choice_set_id', 'related_object_type_id', name=_('Type Options')),
|
||||
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||
)
|
||||
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
required=False,
|
||||
label=_('Related object type')
|
||||
label=_('Object types'),
|
||||
)
|
||||
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.public(),
|
||||
required=False,
|
||||
label=_('Related object type'),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=CustomFieldTypeChoices,
|
||||
@@ -147,12 +150,12 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = CustomLink
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'enabled', 'new_window', 'weight', name=_('Attributes')),
|
||||
FieldSet('object_type_id', 'enabled', 'new_window', 'weight', name=_('Attributes')),
|
||||
)
|
||||
object_type = ContentTypeMultipleChoiceField(
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
label=_('Enabled'),
|
||||
@@ -251,12 +254,12 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = SavedFilter
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('object_type', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||
FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')),
|
||||
)
|
||||
object_type = ContentTypeMultipleChoiceField(
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.public(),
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
label=_('Enabled'),
|
||||
@@ -521,7 +524,7 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ConfigTemplate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
|
||||
FieldSet('data_source_id', 'data_file_id', 'auto_sync_enabled', name=_('Data')),
|
||||
FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering'))
|
||||
)
|
||||
data_source_id = DynamicModelMultipleChoiceField(
|
||||
@@ -537,6 +540,13 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
'source_id': '$data_source_id'
|
||||
}
|
||||
)
|
||||
auto_sync_enabled = forms.NullBooleanField(
|
||||
label=_('Auto sync enabled'),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(ConfigTemplate)
|
||||
mime_type = forms.CharField(
|
||||
required=False,
|
||||
|
||||
@@ -668,6 +668,10 @@ class ConfigTemplateTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name=_('Synced')
|
||||
)
|
||||
auto_sync_enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Auto Sync Enabled'),
|
||||
orderable=False,
|
||||
)
|
||||
mime_type = tables.Column(
|
||||
verbose_name=_('MIME Type')
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
register = template.Library()
|
||||
@@ -8,4 +10,16 @@ register = template.Library()
|
||||
def render_widget(context, widget):
|
||||
request = context['request']
|
||||
|
||||
return widget.render(request)
|
||||
try:
|
||||
return widget.render(request)
|
||||
except Exception as e:
|
||||
message1 = _('An error was encountered when attempting to render this widget:')
|
||||
message2 = _('Please try reconfiguring the widget, or remove it from your dashboard.')
|
||||
return mark_safe(f"""
|
||||
<p>
|
||||
<span class="text-danger"><i class="mdi mdi-alert"></i></span>
|
||||
{message1}
|
||||
</p>
|
||||
<p class="font-monospace ps-3">{e}</p>
|
||||
<p>{message2}</p>
|
||||
""")
|
||||
|
||||
@@ -936,18 +936,13 @@ class ScriptTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.add_permissions('extras.view_script')
|
||||
|
||||
# Monkey-patch the Script model to return our TestScriptClass above
|
||||
Script.python_class = self.python_class
|
||||
|
||||
def test_get_script(self):
|
||||
module = ScriptModule.objects.get(
|
||||
file_root=ManagedFileRootPathChoices.SCRIPTS,
|
||||
file_path='script.py',
|
||||
)
|
||||
script = module.scripts.all().first()
|
||||
url = reverse('extras-api:script-detail', kwargs={'pk': script.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
response = self.client.get(self.url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.TestScriptClass.Meta.name)
|
||||
self.assertEqual(response.data['vars']['var1'], 'StringVar')
|
||||
|
||||
Reference in New Issue
Block a user