示例#1
0
    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}
示例#2
0
 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)
示例#3
0
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.'))
示例#4
0
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
示例#5
0
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}
示例#6
0
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')
示例#7
0
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'
            },
        }
示例#8
0
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
示例#9
0
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)
示例#10
0
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
示例#11
0
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
示例#12
0
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)
示例#13
0
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