diff --git a/debian/changelog b/debian/changelog index 03d60072cb..f12cd1c70b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,23 @@ +ralph (20241118.1) bionic; urgency=medium + + [ Olga Matyla ] + * Add property_of to invoice report (#3864) + * Fix selected count (#3865) + + [ Paweł Szulc ] + * Fix renderer classes ordering + + -- Paweł Szulc Mon, 18 Nov 2024 12:48:57 +0000 + +ralph (20241108.1) bionic; urgency=medium + + * Allow updating dchosts via dchost-list endpoint + * Fix browsable api + * Fix flake + * Add 404 if no serializer found for dc-hosts endpoint + + -- Paweł Szulc Fri, 08 Nov 2024 11:09:13 +0000 + ralph (20241104.2) bionic; urgency=medium [ awieckowski ] diff --git a/src/ralph/admin/templates/admin/actions.html b/src/ralph/admin/templates/admin/actions.html index a52fcc0f9c..90f4d87299 100644 --- a/src/ralph/admin/templates/admin/actions.html +++ b/src/ralph/admin/templates/admin/actions.html @@ -11,10 +11,7 @@

{% trans "Actions" %}

{% trans "Go" %} {% if actions_selection_counter %} - - {{ selection_note }} + {{ selection_note }} {% endif %} {% endif %} diff --git a/src/ralph/assets/admin.py b/src/ralph/assets/admin.py index 20cc176a7a..ddabef29fb 100644 --- a/src/ralph/assets/admin.py +++ b/src/ralph/assets/admin.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from django.db.models import Count +from django.forms import BaseInlineFormSet from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -129,11 +130,17 @@ class ServiceEnvironmentAdmin(CustomFieldValueAdminMixin, RalphAdmin): fields = ('service', 'environment', 'remarks', 'tags') +class PolymorphicInlineFormset(BaseInlineFormSet): + def get_queryset(self): + return super().get_queryset()[:] + + class ServiceEnvironmentInline(RalphTabularInline): model = ServiceEnvironment raw_id_fields = ['environment'] fields = ('environment',) min_num = 1 + formset = PolymorphicInlineFormset class BaseObjectsList(ScanStatusInTableMixin, Table): diff --git a/src/ralph/assets/api/views.py b/src/ralph/assets/api/views.py index 10514fbce7..0e851db8c0 100644 --- a/src/ralph/assets/api/views.py +++ b/src/ralph/assets/api/views.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import django_filters from django.db.models import Prefetch -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.permissions import SAFE_METHODS from ralph.api import RalphAPIViewSet from ralph.api.filters import BooleanFilter @@ -10,9 +11,12 @@ from ralph.assets.api import serializers from ralph.assets.api.filters import NetworkableObjectFilters from ralph.assets.models import BaseObject +from ralph.data_center.models import Cluster, DataCenterAsset +from ralph.lib.api.utils import renderer_classes_without_form from ralph.licences.api import BaseObjectLicenceViewSet from ralph.licences.models import BaseObjectLicence from ralph.networks.models import IPAddress +from ralph.virtual.models import CloudHost, VirtualServer class BusinessSegmentViewSet(RalphAPIViewSet): @@ -258,7 +262,8 @@ class DCHostViewSet(BaseObjectViewSetMixin, RalphAPIViewSet): BaseObject.polymorphic_objects ) serializer_class = serializers.DCHostSerializer - http_method_names = ["get", "options", "head"] + renderer_classes = renderer_classes_without_form(RalphAPIViewSet.renderer_classes) + http_method_names = ["get", "options", "head", "patch", "post"] filter_fields = [ "id", "service_env", @@ -296,6 +301,28 @@ class DCHostViewSet(BaseObjectViewSetMixin, RalphAPIViewSet): } additional_filter_class = DCHostFilterSet + def get_serializer_class(self, *args, **kwargs): + if self.request.method not in SAFE_METHODS: + try: + obj_ = self.get_object() + if isinstance(obj_, VirtualServer): + from ralph.virtual.api import VirtualServerSaveSerializer + return VirtualServerSaveSerializer + elif isinstance(obj_, DataCenterAsset): + from ralph.data_center.api.serializers import DataCenterAssetSaveSerializer + return DataCenterAssetSaveSerializer + elif isinstance(obj_, CloudHost): + from ralph.virtual.api import SaveCloudHostSerializer + return SaveCloudHostSerializer + elif isinstance(obj_, Cluster): + from ralph.data_center.api.serializers import ClusterSerializer + return ClusterSerializer + else: + raise NotFound() + except AssertionError: # for some reason when opening browsable api this raises + pass + return serializers.DCHostSerializer + def get_queryset(self): return ( self.queryset.dc_hosts() diff --git a/src/ralph/assets/invoice_report.py b/src/ralph/assets/invoice_report.py index ee828377d1..bcfbca8c90 100644 --- a/src/ralph/assets/invoice_report.py +++ b/src/ralph/assets/invoice_report.py @@ -27,7 +27,7 @@ class InvoiceReportMixin(object): _invoice_report_select_related = [] _invoice_report_common_fields = [ - 'invoice_no', 'invoice_date', 'provider', 'price_currency' + 'invoice_no', 'invoice_date', 'provider', 'price_currency', 'property_of__name' ] _price_field = 'price' _invoice_report_name = 'invoice' @@ -122,6 +122,7 @@ def _get_report_data(self, request, queryset): 'datetime': datetime.datetime.now().strftime( self._invoice_report_datetime_format ), + 'property_of': first_item.property_of.name, }, 'items': list(map(self._parse_item, queryset)), 'sum_price': str( diff --git a/src/ralph/assets/tests/test_api.py b/src/ralph/assets/tests/test_api.py index cb3cd229fd..9420ba5f7a 100644 --- a/src/ralph/assets/tests/test_api.py +++ b/src/ralph/assets/tests/test_api.py @@ -958,8 +958,8 @@ def test_get_dc_hosts_list(self): VirtualServerFullFactory.create_batch(20, parent=dc_assets[0]) CloudHostFullFactory.create_batch(20, hypervisor=dc_assets[0]) url = reverse('dchost-list') + "?limit=100" - with self.assertNumQueries(31): - response = self.client.get(url, format='json') + with self.assertQueriesMoreOrLess(30, plus_minus=1): + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 63) @@ -1089,6 +1089,31 @@ def test_filter_by_env_name(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) + def test_patch_dchost_virtual_server(self): + new_hypervisor = DataCenterAssetFullFactory() + url = reverse('dchost-detail', args=(self.virtual.id,)) + data = { + 'hostname': 'new-hostname', + 'hypervisor': new_hypervisor.id, + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.virtual.refresh_from_db() + self.assertEqual(self.virtual.hostname, 'new-hostname') + self.assertEqual(self.virtual.parent.id, new_hypervisor.id) + + def test_patch_dchost_cloudhost(self): + new_hypervisor = DataCenterAssetFullFactory() + url = reverse('dchost-detail', args=(self.cloud_host.id,)) + data = { + 'hostname': 'new-hostname', + 'hypervisor': new_hypervisor.id, + } + response = self.client.patch(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.cloud_host.refresh_from_db() + self.assertEqual(self.cloud_host.hostname, 'new-hostname') + self.assertEqual(self.cloud_host.hypervisor.id, new_hypervisor.id) class ConfigurationModuleAPITests(RalphAPITestCase): def setUp(self): diff --git a/src/ralph/lib/api/utils.py b/src/ralph/lib/api/utils.py index 0bde48d5f3..fa7c652d88 100644 --- a/src/ralph/lib/api/utils.py +++ b/src/ralph/lib/api/utils.py @@ -120,7 +120,10 @@ def render_form_for_serializer(self, serializer): def renderer_classes_without_form(renderer_classes): - return [OnlyRawBrowsableAPIRenderer] + [ - rc for rc in renderer_classes - if not isinstance(rc(), BrowsableAPIRenderer) - ] + def _gen(): + for rc in renderer_classes: + if not isinstance(rc(), BrowsableAPIRenderer): + yield rc + else: + yield OnlyRawBrowsableAPIRenderer + return [rc for rc in _gen()]