mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-22 21:32:23 -06:00
Merge v2.9.4 release
This commit is contained in:
@@ -57,24 +57,30 @@ class TaggedObjectSerializer(serializers.Serializer):
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
tags = validated_data.pop('tags', None)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# Cache tags on instance for change logging
|
||||
instance._tags = tags
|
||||
instance._tags = tags or []
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
else:
|
||||
instance.tags.clear()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
@@ -236,6 +237,16 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
|
||||
@@ -353,10 +353,11 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
user = DynamicModelMultipleChoiceField(
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
display_field='username',
|
||||
label='User',
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import time
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from extras.reports import get_reports
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import JobResult
|
||||
from extras.reports import get_reports, run_report
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -20,15 +25,33 @@ class Command(BaseCommand):
|
||||
for report in report_list:
|
||||
if module_name in options['reports'] or report.full_name in options['reports']:
|
||||
|
||||
# Run the report and create a new ReportResult
|
||||
# Run the report and create a new JobResult
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
|
||||
)
|
||||
report.run()
|
||||
|
||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||
job_result = JobResult.enqueue_job(
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
None
|
||||
)
|
||||
|
||||
# Wait on the job to finish
|
||||
while job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
|
||||
time.sleep(1)
|
||||
job_result = JobResult.objects.get(pk=job_result.pk)
|
||||
|
||||
# Report on success/failure
|
||||
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
|
||||
for test_name, attrs in report.result.data.items():
|
||||
if job_result.status == JobResultStatusChoices.STATUS_FAILED:
|
||||
status = self.style.ERROR('FAILED')
|
||||
elif job_result == JobResultStatusChoices.STATUS_ERRORED:
|
||||
status = self.style.ERROR('ERRORED')
|
||||
else:
|
||||
status = self.style.SUCCESS('SUCCESS')
|
||||
|
||||
for test_name, attrs in job_result.data.items():
|
||||
self.stdout.write(
|
||||
"\t{}: {} success, {} info, {} warning, {} failure".format(
|
||||
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
|
||||
@@ -37,6 +60,9 @@ class Command(BaseCommand):
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
|
||||
)
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] {}: Duration {}".format(timezone.now(), report.full_name, job_result.duration)
|
||||
)
|
||||
|
||||
# Wrap things up
|
||||
self.stdout.write(
|
||||
|
||||
@@ -55,7 +55,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0020_custom_field_data'),
|
||||
('dcim', '0116_custom_field_data'),
|
||||
('dcim', '0117_custom_field_data'),
|
||||
('extras', '0050_customfield_add_choices'),
|
||||
('ipam', '0038_custom_field_data'),
|
||||
('secrets', '0010_custom_field_data'),
|
||||
|
||||
@@ -381,12 +381,11 @@ class ObjectChangeTestCase(TestCase):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
# TODO: Merge #5167 from develop
|
||||
# def test_user(self):
|
||||
# params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
# params = {'user': ['user1', 'user2']}
|
||||
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
def test_user(self):
|
||||
params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'user': ['user1', 'user2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_user_name(self):
|
||||
params = {'user_name': ['user1', 'user2']}
|
||||
|
||||
@@ -59,3 +59,21 @@ class TaggedItemTest(APITestCase):
|
||||
sorted([t.name for t in site.tags.all()]),
|
||||
sorted(["Foo", "Bar", "New Tag"])
|
||||
)
|
||||
|
||||
def test_clear_tagged_item(self):
|
||||
site = Site.objects.create(
|
||||
name='Test Site',
|
||||
slug='test-site'
|
||||
)
|
||||
site.tags.add("Foo", "Bar", "Baz")
|
||||
data = {
|
||||
'tags': []
|
||||
}
|
||||
self.add_permissions('dcim.change_site')
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data['tags']), 0)
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(len(site.tags.all()), 0)
|
||||
|
||||
@@ -315,7 +315,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
||||
Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
|
||||
"""
|
||||
def get_required_permission(self):
|
||||
return 'extras.view_reportresult'
|
||||
return 'extras.view_report'
|
||||
|
||||
def get(self, request):
|
||||
|
||||
@@ -347,7 +347,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
|
||||
Display a single Report and its associated JobResult (if any).
|
||||
"""
|
||||
def get_required_permission(self):
|
||||
return 'extras.view_reportresult'
|
||||
return 'extras.view_report'
|
||||
|
||||
def get(self, request, module, name):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user