def setUp(self): from waldur_core.structure.tests.factories import UserFactory self.user = UserFactory(is_staff=True) self.request = APIRequestFactory().get('/') self.request.user = self.user self.field = GenericRelatedField(related_models=get_loggable_models()) self.field.root._context = {'request': self.request}
def _get_field_type(self, field): if isinstance(field, MappedMultipleChoiceFilter): return 'choice(%s)' % ', '.join(["'%s'" % f for f in sorted(field.mapped_to_model)]) if isinstance(field, ChoiceField): return 'choice(%s)' % ', '.join(["'%s'" % f for f in sorted(field.choices)]) if isinstance(field, HyperlinkedRelatedField): path = self.VIEWS.get(field.view_name) if path: return 'link to %s' % path if isinstance(field, GenericRelatedField): paths = [self.VIEWS.get(GenericRelatedField()._get_url(m())) for m in field.related_models] path = ', '.join([m for m in paths if m]) if path: return 'link to any: %s' % path if isinstance(field, ContentTypeFilter): return 'string in form <app_label>.<model_name>' if isinstance(field, ModelSerializer): fields = {f['name']: f['type'] for f in self.get_serializer_fields(field) if not f['readonly']} return '{%s}' % ', '.join(['%s: %s' % (k, v) for k, v in fields.items()]) if isinstance(field, ModelMultipleChoiceFilter): return self._get_field_type(field.field) if isinstance(field, ListSerializer): return 'list of [%s]' % self._get_field_type(field.child) if isinstance(field, ManyRelatedField): return 'list of [%s]' % self._get_field_type(field.child_relation) if isinstance(field, ModelField): return self._get_field_type(field.model_field) name = field.__class__.__name__ return self.FIELDS.get(name, name)
class PriceListItemSerializer(AugmentedSerializerMixin, serializers.HyperlinkedModelSerializer): service = GenericRelatedField( related_models=structure_models.Service.get_all_models()) default_price_list_item = serializers.HyperlinkedRelatedField( view_name='defaultpricelistitem-detail', lookup_field='uuid', queryset=models.DefaultPriceListItem.objects.all().select_related( 'resource_content_type')) class Meta: model = models.PriceListItem fields = ('url', 'uuid', 'units', 'value', 'service', 'default_price_list_item') extra_kwargs = { 'url': { 'lookup_field': 'uuid' }, 'default_price_list_item': { 'lookup_field': 'uuid' } } protected_fields = ('service', 'default_price_list_item') def create(self, validated_data): try: return super(PriceListItemSerializer, self).create(validated_data) except IntegrityError: raise serializers.ValidationError( _('Price list item for service already exists.'))
class AlertSerializer(serializers.HyperlinkedModelSerializer): scope = GenericRelatedField(related_models=utils.get_loggable_models()) severity = MappedChoiceField( choices=[(v, k) for k, v in models.Alert.SeverityChoices.CHOICES], choice_mappings={v: k for k, v in models.Alert.SeverityChoices.CHOICES}, ) context = JsonField(read_only=True) class Meta(object): model = models.Alert fields = ( 'url', 'uuid', 'alert_type', 'message', 'severity', 'scope', 'created', 'closed', 'context', 'acknowledged', ) read_only_fields = ('uuid', 'created', 'closed') extra_kwargs = { 'url': {'lookup_field': 'uuid'}, } def create(self, validated_data): try: alert, created = loggers.AlertLogger().process( severity=validated_data['severity'], message_template=validated_data['message'], scope=validated_data['scope'], alert_type=validated_data['alert_type'], ) except IntegrityError: # In case of simultaneous requests serializer validation can pass for both alerts, # so we need to handle DB IntegrityError separately. raise serializers.ValidationError(_('Alert with given type and scope already exists.')) else: return alert
class PriceEstimateSerializer(AugmentedSerializerMixin, serializers.HyperlinkedModelSerializer): scope = GenericRelatedField(related_models=models.PriceEstimate.get_estimated_models()) scope_name = serializers.SerializerMethodField() scope_type = serializers.SerializerMethodField() resource_type = serializers.SerializerMethodField() consumption_details = serializers.SerializerMethodField( help_text=_('How much of each consumables were used by resource.')) def __init__(self, *args, **kwargs): depth = kwargs.get('context', {}).pop('depth', 0) # allow to modify depth dynamically self.Meta.depth = depth super(PriceEstimateSerializer, self).__init__(*args, **kwargs) class Meta(object): model = models.PriceEstimate fields = ('url', 'uuid', 'scope', 'total', 'consumed', 'month', 'year', 'scope_name', 'scope_type', 'resource_type', 'consumption_details', 'children') extra_kwargs = { 'url': {'lookup_field': 'uuid'}, } def get_fields(self): display_children = self.Meta.depth > 0 fields = super(PriceEstimateSerializer, self).get_fields() if not display_children: del fields['children'] return fields def build_nested_field(self, field_name, relation_info, nested_depth): """ Use PriceEstimateSerializer to serialize estimate children """ if field_name != 'children': return super(PriceEstimateSerializer, self).build_nested_field(field_name, relation_info, nested_depth) field_class = self.__class__ field_kwargs = {'read_only': True, 'many': True, 'context': {'depth': nested_depth - 1}} return field_class, field_kwargs def get_scope_name(self, obj): if obj.scope: return getattr(obj.scope, 'name', six.text_type(obj.scope)) if obj.details: return obj.details.get('scope_name') def get_scope_type(self, obj): return ScopeTypeFilterBackend.get_scope_type(obj.content_type.model_class()) def get_resource_type(self, obj): if obj.is_resource_estimate(): return SupportedServices.get_name_for_model(obj.content_type.model_class()) def get_consumption_details(self, obj): try: consumption_details = obj.consumption_details except models.ConsumptionDetails.DoesNotExist: return consumed_in_month = consumption_details.consumed_in_month consumable_items = consumed_in_month.keys() pretty_names = models.DefaultPriceListItem.get_consumable_items_pretty_names( obj.content_type, consumable_items) return {pretty_names[item]: consumed_in_month[item] for item in consumable_items}
class CategoryComponentUsageSerializer(core_serializers.RestrictedSerializerMixin, BaseComponentSerializer, serializers.ModelSerializer): category_title = serializers.ReadOnlyField(source='component.category.title') category_uuid = serializers.ReadOnlyField(source='component.category.uuid') scope = GenericRelatedField(related_models=(structure_models.Project, structure_models.Customer)) class Meta(object): model = models.CategoryComponentUsage fields = ('name', 'type', 'measured_unit', 'category_title', 'category_uuid', 'date', 'reported_usage', 'fixed_usage', 'scope')
class QuotaSerializer(serializers.HyperlinkedModelSerializer): scope = GenericRelatedField(related_models=utils.get_models_with_quotas(), read_only=True) class Meta: model = models.Quota fields = ('url', 'uuid', 'name', 'limit', 'usage', 'scope', 'threshold') read_only_fields = ('uuid', 'name', 'usage') extra_kwargs = { 'url': { 'lookup_field': 'uuid' }, }
class DailyHistoryQuotaSerializer(serializers.Serializer): scope = GenericRelatedField(related_models=(Project, Customer)) quota_names = serializers.ListField(child=serializers.CharField(), required=False) start = serializers.DateField(format='%Y-%m-%d', required=False) end = serializers.DateField(format='%Y-%m-%d', required=False) def validate(self, attrs): if 'quota_names' not in attrs: attrs['quota_names'] = attrs['scope'].get_quotas_names() if 'end' not in attrs: attrs['end'] = timezone.now().date() if 'start' not in attrs: attrs['start'] = timezone.now().date() - timedelta(days=30) if attrs['start'] >= attrs['end']: raise serializers.ValidationError( _('Invalid period specified. `start` should be lesser than `end`.') ) return attrs
class GenericRelatedFieldTest(APITransactionTestCase): def setUp(self): from waldur_core.structure.tests.factories import UserFactory self.user = UserFactory(is_staff=True) self.request = APIRequestFactory().get('/') self.request.user = self.user self.field = GenericRelatedField(related_models=get_loggable_models()) self.field.root._context = {'request': self.request} def test_if_related_object_exists_it_is_deserialized(self): from waldur_core.structure.tests.factories import CustomerFactory customer = CustomerFactory() valid_url = CustomerFactory.get_url(customer) self.assertEqual(self.field.to_internal_value(valid_url), customer) def test_if_related_object_does_not_exist_validation_error_is_raised(self): from waldur_core.structure.tests.factories import CustomerFactory customer = CustomerFactory() valid_url = CustomerFactory.get_url(customer) customer.delete() self.assertRaises(serializers.ValidationError, self.field.to_internal_value, valid_url) def test_if_user_does_not_have_permissions_for_related_object_validation_error_is_raised( self, ): from waldur_core.structure.tests.factories import CustomerFactory customer = CustomerFactory() valid_url = CustomerFactory.get_url(customer) self.user.is_staff = False self.user.save() self.assertRaises(serializers.ValidationError, self.field.to_internal_value, valid_url) def test_if_uuid_is_invalid_validation_error_is_raised(self): invalid_url = 'https://example.com/api/customers/invalid/' self.assertRaises(serializers.ValidationError, self.field.to_internal_value, invalid_url)
class OfferingSerializer(core_serializers.AugmentedSerializerMixin, core_serializers.RestrictedSerializerMixin, serializers.HyperlinkedModelSerializer): attributes = serializers.JSONField(required=False) options = serializers.JSONField(required=False) components = OfferingComponentSerializer(required=False, many=True) geolocations = core_serializers.GeoLocationField(required=False) order_item_count = serializers.SerializerMethodField() plans = NestedPlanSerializer(many=True, required=False) screenshots = NestedScreenshotSerializer(many=True, read_only=True) state = serializers.ReadOnlyField(source='get_state_display') scope = GenericRelatedField( related_models=plugins.manager.get_scope_models, required=False) scope_uuid = serializers.ReadOnlyField(source='scope.uuid') class Meta(object): model = models.Offering fields = ('url', 'uuid', 'created', 'name', 'description', 'full_description', 'customer', 'customer_uuid', 'customer_name', 'category', 'category_uuid', 'category_title', 'rating', 'attributes', 'options', 'components', 'geolocations', 'state', 'native_name', 'native_description', 'vendor_details', 'thumbnail', 'order_item_count', 'plans', 'screenshots', 'type', 'shared', 'scope', 'scope_uuid') related_paths = { 'customer': ('uuid', 'name'), 'category': ('uuid', 'title'), } protected_fields = ('customer', 'type', 'scope') read_only_fields = ('state', ) extra_kwargs = { 'url': { 'lookup_field': 'uuid', 'view_name': 'marketplace-offering-detail' }, 'customer': { 'lookup_field': 'uuid', 'view_name': 'customer-detail' }, 'category': { 'lookup_field': 'uuid', 'view_name': 'marketplace-category-detail' }, } def get_order_item_count(self, offering): try: return offering.quotas.get(name='order_item_count').usage except ObjectDoesNotExist: return 0 def validate(self, attrs): if not self.instance: structure_permissions.is_owner(self.context['request'], None, attrs['customer']) offering_attributes = attrs.get('attributes') if offering_attributes is not None: if not isinstance(offering_attributes, dict): raise rf_exceptions.ValidationError({ 'attributes': _('Dictionary is expected.'), }) category = attrs.get('category', getattr(self.instance, 'category', None)) self._validate_attributes(offering_attributes, category) self._validate_plans(attrs) self._validate_scope(attrs) return attrs def validate_type(self, offering_type): if offering_type not in plugins.manager.backends.keys(): raise rf_exceptions.ValidationError(_('Invalid value.')) return offering_type def _validate_attributes(self, offering_attributes, category): offering_attribute_keys = offering_attributes.keys() required_category_attributes = list( models.Attribute.objects.filter(section__category=category, required=True)) unfilled_attributes = { attr.key for attr in required_category_attributes } - set(offering_attribute_keys) if unfilled_attributes: raise rf_exceptions.ValidationError({ 'attributes': _('Required fields %s are not filled' % list(unfilled_attributes)) }) category_attributes = list( models.Attribute.objects.filter(section__category=category, key__in=offering_attribute_keys)) for key, value in offering_attributes.items(): match_attributes = filter(lambda a: a.key == key, category_attributes) attribute = match_attributes[0] if match_attributes else None if attribute: klass = attribute_types.get_attribute_type(attribute.type) if klass: try: klass.validate( value, list( attribute.options.values_list('key', flat=True))) except ValidationError as e: err = rf_exceptions.ValidationError( {'attributes': e.message}) raise err def validate_options(self, options): serializer = OfferingOptionsSerializer(data=options) serializer.is_valid(raise_exception=True) return options def _validate_plans(self, attrs): custom_components = attrs.get('components') offering_type = attrs.get('type', getattr(self.instance, 'type', None)) builtin_components = plugins.manager.get_components(offering_type) valid_types = set() fixed_types = set() if builtin_components and custom_components: raise serializers.ValidationError( {'components': _('Extra components are not allowed.')}) elif builtin_components: valid_types = {component.type for component in builtin_components} fixed_types = { component.type for component in plugins.manager.get_components(offering_type) if component.billing_type == models.OfferingComponent.BillingTypes.FIXED } elif custom_components: valid_types = { component['type'] for component in custom_components } fixed_types = { component['type'] for component in custom_components if component['billing_type'] == models.OfferingComponent.BillingTypes.FIXED } for plan in attrs.get('plans', []): prices = plan.get('prices', {}) price_components = set(prices.keys()) if price_components != valid_types: raise serializers.ValidationError( {'plans': _('Invalid price components.')}) quotas = plan.get('quotas', {}) quota_components = set(quotas.keys()) if quota_components != fixed_types: raise serializers.ValidationError( {'plans': _('Invalid quota components.')}) plan['unit_price'] = sum(prices[component] * quotas[component] for component in fixed_types) def _validate_scope(self, attrs): offering_scope = attrs.get('scope') offering_type = attrs.get('type') if offering_scope: offering_scope_model = plugins.manager.get_scope_model( offering_type) if offering_scope_model: if not isinstance(offering_scope, offering_scope_model): raise serializers.ValidationError( {'scope': _('Invalid scope model.')}) @transaction.atomic def create(self, validated_data): plans = validated_data.pop('plans', []) custom_components = validated_data.pop('components', []) offering = super(OfferingSerializer, self).create(validated_data) fixed_components = plugins.manager.get_components(offering.type) for component_data in fixed_components: models.OfferingComponent.objects.create(offering=offering, **component_data._asdict()) for component_data in custom_components: models.OfferingComponent.objects.create(offering=offering, **component_data) components = { component.type: component for component in offering.components.all() } for plan_data in plans: self._create_plan(offering, plan_data, components) return offering def _create_plan(self, offering, plan_data, components): quotas = plan_data.pop('quotas', {}) prices = plan_data.pop('prices', {}) plan = models.Plan.objects.create(offering=offering, **plan_data) for name, component in components.items(): models.PlanComponent.objects.create( plan=plan, component=component, amount=quotas.get(name) or 0, price=prices[name], ) def update(self, instance, validated_data): # TODO: Implement support for nested plan update validated_data.pop('plans', []) validated_data.pop('components', []) offering = super(OfferingSerializer, self).update(instance, validated_data) return offering
class HostSerializer(structure_serializers.BaseResourceSerializer): service = serializers.HyperlinkedRelatedField( source='service_project_link.service', view_name='zabbix-detail', read_only=True, lookup_field='uuid') service_project_link = serializers.HyperlinkedRelatedField( view_name='zabbix-spl-detail', queryset=models.ZabbixServiceProjectLink.objects.all(), allow_null=True, required=False, ) # visible name could be populated from scope, so we need to mark it as not required visible_name = serializers.CharField( required=False, max_length=models.Host.VISIBLE_NAME_MAX_LENGTH) scope = GenericRelatedField( related_models=structure_models.ResourceMixin.get_all_models(), required=False) templates = NestedTemplateSerializer( queryset=models.Template.objects.all().prefetch_related( 'items', 'children'), many=True, required=False) status = MappedChoiceField( choices={v: v for _, v in models.Host.Statuses.CHOICES}, choice_mappings={v: k for k, v in models.Host.Statuses.CHOICES}, read_only=True, ) interface_ip = serializers.IPAddressField( allow_blank=True, required=False, write_only=True, help_text='IP of host interface.') interface_parameters = serializers.JSONField(read_only=True) class Meta(structure_serializers.BaseResourceSerializer.Meta): model = models.Host view_name = 'zabbix-host-detail' fields = structure_serializers.BaseResourceSerializer.Meta.fields + ( 'visible_name', 'host_group_name', 'scope', 'templates', 'error', 'status', 'interface_ip', 'interface_parameters', ) read_only_fields = structure_serializers.BaseResourceSerializer.Meta.read_only_fields + ( 'error', 'interface_parameters') protected_fields = structure_serializers.BaseResourceSerializer.Meta.protected_fields + ( 'interface_ip', 'visible_name') def get_resource_fields(self): return super(HostSerializer, self).get_resource_fields() + ['scope'] def validate(self, attrs): attrs = super(HostSerializer, self).validate(attrs) # model validation if self.instance is not None: for name, value in attrs.items(): setattr(self.instance, name, value) self.instance.clean() else: service_settings = attrs.get( 'service_project_link').service.settings if service_settings.state == structure_models.ServiceSettings.States.ERRED: raise serializers.ValidationError( 'It is impossible to create host if service is in ERRED state.' ) if not attrs.get('visible_name'): if 'scope' not in attrs: raise serializers.ValidationError( 'Visible name or scope should be defined.') # initiate name and visible name from scope if it is defined attrs[ 'visible_name'] = models.Host.get_visible_name_from_scope( attrs['scope']) spl = attrs['service_project_link'] if models.Host.objects.filter( service_project_link__service__settings=spl.service. settings, visible_name=attrs['visible_name']).exists(): raise serializers.ValidationError( {'visible_name': 'Visible name should be unique.'}) instance = models.Host( **{ k: v for k, v in attrs.items() if k not in ('templates', 'interface_ip') }) instance.clean() spl = attrs.get( 'service_project_link') or self.instance.service_project_link templates = attrs.get('templates', []) parents = {} # dictionary <parent template: child template> for template in templates: if template.settings != spl.service.settings: raise serializers.ValidationError({ 'templates': 'Template "%s" and host belong to different service settings.' % template.name }) for child in template.children.all(): if child in templates: message = 'Template "%s" is already registered as child of template "%s"' % ( child.name, template.name) raise serializers.ValidationError({'templates': message}) for parent in template.parents.all(): if parent in parents: message = 'Templates %s and %s belong to the same parent %s' % ( template, parents[parent], parent) raise serializers.ValidationError({'templates': message}) else: parents[parent] = template for template in templates: if template in parents: message = 'Template "%s" is already registered as a parent of template "%s"' % \ (template, parents[template]) raise serializers.ValidationError({'templates': message}) return attrs def create(self, validated_data): # define interface parameters based on settings and user input spl = validated_data['service_project_link'] interface_parameters = spl.service.settings.get_option( 'interface_parameters') scope = validated_data.get('scope') interface_ip = validated_data.pop('interface_ip', None) or getattr( scope, 'internal_ips', None) if interface_ip: # Note, that we're not supporting multiple network interfaces for single host yet. # IPv6 is not supported yet too. if isinstance(interface_ip, list): interface_ip = interface_ip[0] interface_parameters['ip'] = interface_ip validated_data['interface_parameters'] = interface_parameters # populate templates templates = validated_data.pop('templates', None) with transaction.atomic(): host = super(HostSerializer, self).create(validated_data) # get default templates from service settings if they are not defined if templates is None: templates = models.Template.objects.filter( settings=host.service_project_link.service.settings, name__in=host.service_project_link.service.settings. get_option('templates_names'), ) for template in templates: host.templates.add(template) return host def update(self, host, validated_data): templates = validated_data.pop('templates', None) with transaction.atomic(): host = super(HostSerializer, self).update(host, validated_data) if templates is not None: host.templates.clear() for template in templates: host.templates.add(template) return host
class EventSerializer(serializers.Serializer): level = serializers.ChoiceField( choices=['debug', 'info', 'warning', 'error']) message = serializers.CharField() scope = GenericRelatedField(related_models=utils.get_loggable_models(), required=False)
class OfferingSerializer(core_serializers.AugmentedSerializerMixin, core_serializers.RestrictedSerializerMixin, serializers.HyperlinkedModelSerializer): attributes = serializers.JSONField(required=False) options = serializers.JSONField(required=False) components = OfferingComponentSerializer(required=False, many=True) geolocations = core_serializers.GeoLocationField(required=False) order_item_count = serializers.SerializerMethodField() plans = NestedPlanSerializer(many=True, required=False) screenshots = NestedScreenshotSerializer(many=True, read_only=True) state = serializers.ReadOnlyField(source='get_state_display') scope = GenericRelatedField(read_only=True) scope_uuid = serializers.ReadOnlyField(source='scope.uuid') service_attributes = serializers.JSONField(required=False, write_only=True) files = NestedOfferingFileSerializer(many=True, read_only=True) class Meta(object): model = models.Offering fields = ('url', 'uuid', 'created', 'name', 'description', 'full_description', 'terms_of_service', 'customer', 'customer_uuid', 'customer_name', 'category', 'category_uuid', 'category_title', 'rating', 'attributes', 'options', 'components', 'geolocations', 'state', 'native_name', 'native_description', 'vendor_details', 'thumbnail', 'order_item_count', 'plans', 'screenshots', 'type', 'shared', 'billable', 'service_attributes', 'scope', 'scope_uuid', 'files') related_paths = { 'customer': ('uuid', 'name'), 'category': ('uuid', 'title'), } protected_fields = ('customer', 'type', 'scope') read_only_fields = ('state',) extra_kwargs = { 'url': {'lookup_field': 'uuid', 'view_name': 'marketplace-offering-detail'}, 'customer': {'lookup_field': 'uuid', 'view_name': 'customer-detail'}, 'category': {'lookup_field': 'uuid', 'view_name': 'marketplace-category-detail'}, } def get_order_item_count(self, offering): try: return offering.quotas.get(name='order_item_count').usage except ObjectDoesNotExist: return 0 def validate(self, attrs): if not self.instance: structure_permissions.is_owner(self.context['request'], None, attrs['customer']) offering_attributes = attrs.get('attributes') if offering_attributes is not None: if not isinstance(offering_attributes, dict): raise rf_exceptions.ValidationError({ 'attributes': _('Dictionary is expected.'), }) category = attrs.get('category', getattr(self.instance, 'category', None)) self._validate_attributes(offering_attributes, category) self._validate_plans(attrs) return attrs def validate_type(self, offering_type): if offering_type not in plugins.manager.backends.keys(): raise rf_exceptions.ValidationError(_('Invalid value.')) return offering_type def _validate_attributes(self, offering_attributes, category): offering_attribute_keys = offering_attributes.keys() required_category_attributes = list(models.Attribute.objects.filter(section__category=category, required=True)) unfilled_attributes = {attr.key for attr in required_category_attributes} - set(offering_attribute_keys) if unfilled_attributes: raise rf_exceptions.ValidationError( {'attributes': _('Required fields %s are not filled' % list(unfilled_attributes))}) category_attributes = list(models.Attribute.objects.filter(section__category=category, key__in=offering_attribute_keys)) for key, value in offering_attributes.items(): match_attributes = filter(lambda a: a.key == key, category_attributes) attribute = match_attributes[0] if match_attributes else None if attribute: klass = attribute_types.get_attribute_type(attribute.type) if klass: try: klass.validate(value, list(attribute.options.values_list('key', flat=True))) except ValidationError as e: err = rf_exceptions.ValidationError({'attributes': e.message}) raise err def validate_options(self, options): serializer = OfferingOptionsSerializer(data=options) serializer.is_valid(raise_exception=True) return serializer.validated_data def _validate_plans(self, attrs): custom_components = attrs.get('components') offering_type = attrs.get('type', getattr(self.instance, 'type', None)) builtin_components = plugins.manager.get_components(offering_type) valid_types = set() fixed_types = set() if builtin_components and custom_components: raise serializers.ValidationError({ 'components': _('Extra components are not allowed.') }) elif builtin_components: valid_types = {component.type for component in builtin_components} fixed_types = {component.type for component in plugins.manager.get_components(offering_type) if component.billing_type == models.OfferingComponent.BillingTypes.FIXED} elif custom_components: valid_types = {component['type'] for component in custom_components} fixed_types = {component['type'] for component in custom_components if component['billing_type'] == models.OfferingComponent.BillingTypes.FIXED} for plan in attrs.get('plans', []): prices = plan.get('prices', {}) price_components = set(prices.keys()) if price_components != valid_types: raise serializers.ValidationError({ 'plans': _('Invalid price components.') }) quotas = plan.get('quotas', {}) quota_components = set(quotas.keys()) if quota_components != fixed_types: raise serializers.ValidationError({ 'plans': _('Invalid quota components.') }) plan['unit_price'] = sum(prices[component] * quotas[component] for component in fixed_types) @transaction.atomic def create(self, validated_data): plans = validated_data.pop('plans', []) custom_components = validated_data.pop('components', []) if len(plans) < 1: raise serializers.ValidationError({ 'plans': _('At least one plan should be specified.') }) offering_type = validated_data.get('type') service_type = plugins.manager.get_service_type(offering_type) if service_type: validated_data = self._create_service(service_type, validated_data) offering = super(OfferingSerializer, self).create(validated_data) fixed_components = plugins.manager.get_components(offering.type) for component_data in fixed_components: models.OfferingComponent.objects.create( offering=offering, **component_data._asdict() ) for component_data in custom_components: models.OfferingComponent.objects.create(offering=offering, **component_data) components = {component.type: component for component in offering.components.all()} for plan_data in plans: self._create_plan(offering, plan_data, components) return offering def _create_service(self, service_type, validated_data): """ Marketplace offering model does not accept service_attributes field as is, therefore we should remove it from validated_data and create service settings object. Then we need to specify created object and offering's scope. """ name = validated_data['name'] service_attributes = validated_data.pop('service_attributes', {}) if not service_attributes: raise ValidationError({ 'service_attributes': _('This field is required.') }) payload = dict( name=name, # It is expected that customer URL is passed to the service settings serializer customer=self.initial_data['customer'], type=service_type, **service_attributes ) serializer_class = SupportedServices.get_service_serializer_for_key(service_type) serializer = serializer_class(data=payload, context=self.context) serializer.is_valid(raise_exception=True) service = serializer.save() # Usually we don't allow users to create new shared service settings via REST API. # That's shared flag is marked as read-only in service settings serializer. # But shared offering should be created with shared service settings. # That's why we set it to shared only after service settings object is created. if validated_data.get('shared'): service.settings.shared = True service.settings.save() # Usually connect shared settings task is called when service is created. # But as we set shared flag after serializer has been executed, # we need to connect shared settings manually. connect_shared_settings(service.settings) validated_data['scope'] = service.settings return validated_data def _create_plan(self, offering, plan_data, components): quotas = plan_data.pop('quotas', {}) prices = plan_data.pop('prices', {}) plan = models.Plan.objects.create(offering=offering, **plan_data) for name, component in components.items(): models.PlanComponent.objects.create( plan=plan, component=component, amount=quotas.get(name) or 0, price=prices[name], ) def update(self, instance, validated_data): # TODO: Implement support for nested plan update validated_data.pop('plans', []) validated_data.pop('components', []) offering = super(OfferingSerializer, self).update(instance, validated_data) return offering