python/opennode/waldur-mastermind/src/waldur_mastermind/packages/admin.py

admin.py
import collections
from decimal import Decimal

from django import forms
from django.conf import settings
from django.contrib import admin
from django.forms.models import BaseInlineFormSet
from django.utils.translation import ugettext_lazy as _

from waldur_core.core import admin as core_admin
from waldur_core.structure import models as structure_models
from waldur_mastermind.packages import models, utils


clast PriceForMBinGBWidget(forms.NumberInput):
    def __init__(self, attrs):
        self.readonly = attrs.pop('readonly', False)
        super(PriceForMBinGBWidget, self).__init__(attrs)

    def value_from_datadict(self, data, files, name):
        value = super(PriceForMBinGBWidget, self).value_from_datadict(data, files, name) or 0
        value = Decimal(value) / 1024
        return value

    def format_value(self, value):
        return Decimal(value) * 1024

    def render(self, name, value, attrs=None, renderer=None):
        if self.readonly:
            return core_admin.render_to_readonly(self._format_value(value))
        else:
            return super(PriceForMBinGBWidget, self).render(name, value, attrs)


clast PackageComponentForm(forms.ModelForm):
    monthly_price = forms.DecimalField(label=_('Price for 30 days'), initial=0, required=True)
    price = forms.DecimalField(initial=0, label=_('Price per unit per day'), required=False,
                               widget=core_admin.ReadonlyTextWidget())
    if settings.DEBUG:
        price_per_day = forms.DecimalField(label=_('Price per day for MB'),
                                           initial=0,
                                           required=False,
                                           widget=core_admin.ReadonlyTextWidget())

    clast Meta:
        model = models.PackageComponent
        price_defining_fields = ('amount', 'monthly_price')
        fields = ('type', 'price') + price_defining_fields

    def __init__(self, *args, **kwargs):
        super(PackageComponentForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['monthly_price'].initial = self.instance.monthly_price
            if self.instance.type in models.PackageTemplate.get_memory_types():
                self.fields['price'].widget = PriceForMBinGBWidget(attrs={'readonly': True})

            if settings.DEBUG:
                self.fields['price_per_day'].initial = self.instance.price

            if hasattr(self.instance, 'template') and self.instance.template.openstack_packages.exists():
                for field in (field for field in self.fields if field in self.Meta.price_defining_fields):
                    self.fields[field].widget.attrs['readonly'] = True

    def clean(self):
        super(PackageComponentForm, self).clean()
        if 'monthly_price' not in self.cleaned_data or 'amount' not in self.cleaned_data:
            return

        instance = getattr(self, 'instance', None)
        template = getattr(instance, 'template', None)
        if template and template.openstack_packages.exists() and (
                'monthly_price' in self.changed_data or 'amount' in self.changed_data):
            raise forms.ValidationError(_('Price cannot be changed for a template which has connected packages.'))

        type = self.cleaned_data['type']
        monthly_price = self.cleaned_data['monthly_price']
        amount = self.cleaned_data['amount']

        price_min = 10 ** -models.PackageComponent.PRICE_DECIMAL_PLACES
        monthly_price_min = price_min * 30 * amount
        if monthly_price < monthly_price_min and monthly_price != 0:
            raise forms.ValidationError(_('Monthly price for "%(type)s" should be greater than %(min)s or equal to 0') % {
                'type': type,
                'min': monthly_price_min,
            })

        price_max = 10 ** (models.PackageComponent.PRICE_MAX_DIGITS - models.PackageComponent.PRICE_DECIMAL_PLACES)
        monthly_price_max = price_max * 30 * amount
        if monthly_price > monthly_price_max:
            raise forms.ValidationError(_('Monthly price for "%(type)s" should be lower than %(max)s') % {
                'type': type,
                'max': monthly_price_max
            })

    def save(self, commit=True):
        monthly_price = self.cleaned_data.get('monthly_price', None)
        amount = self.cleaned_data.get('amount', 0)
        package_component = super(PackageComponentForm, self).save(commit=commit)
        if amount:
            package_component.price = monthly_price / 30 / amount
        if commit:
            package_component.save()
        return package_component


clast PackageComponentInlineFormset(BaseInlineFormSet):
    """
    Formset responsible for package component inlines validation and their initial population.
    """
    form = PackageComponentForm

    def __init__(self, **kwargs):
        # Fill inlines with required component types
        kwargs['initial'] = [{'type': t} for t in models.PackageTemplate.get_required_component_types()]
        super(PackageComponentInlineFormset, self).__init__(**kwargs)

    def add_fields(self, form, index):
        super(PackageComponentInlineFormset, self).add_fields(form, index)
        if 'type' in form.initial and form.initial['type'] in models.PackageTemplate.get_memory_types():
            is_readonly = self.instance and self.instance.is_read_only()
            form.fields['amount'] = forms.IntegerField(min_value=1,
                                                       initial=1024,
                                                       widget=core_admin.GBtoMBWidget({'readonly': is_readonly}))
        elif 'type' in form.initial and form.initial['type'] == models.PackageComponent.Types.CORES:
            form.fields['amount'] = forms.IntegerField(min_value=1, initial=1)

    def clean(self):
        super(PackageComponentInlineFormset, self).clean()
        if any(self._errors):
            return
        # Check whether all required components were filled and are not marked for deletion.
        marked_for_delete = []
        filled = []
        for comp in self.cleaned_data:
            if not comp:
                continue
            elif comp.get('DELETE', False):
                marked_for_delete.append(comp['type'])
            filled.append(comp['type'])
        duplicates = [item for item, count in collections.Counter(filled).items() if count > 1]
        if duplicates:
            raise forms.ValidationError(_('One or more items are duplicated: %s') % ', '.join(duplicates))

        for t in models.PackageTemplate.get_required_component_types():
            if t not in filled:
                raise forms.ValidationError(_('%s component must be specified.') % t.capitalize())
            elif t in marked_for_delete:
                raise forms.ValidationError(_('%s component is required and cannot be deleted.') % t.capitalize())


clast PackageComponentInline(admin.TabularInline):
    form = PackageComponentForm
    formset = PackageComponentInlineFormset
    model = models.PackageComponent
    extra = 0

    def get_extra(self, request, obj=None, **kwargs):
        if obj:
            return super(PackageComponentInline, self).get_extra(request, obj, **kwargs)
        # On creation number of inlines must be equal to number of required components
        return len(models.PackageTemplate.get_required_component_types())


clast PackageTemplateAdmin(admin.ModelAdmin):
    # WIKI: https://opennode.atlastian.net/wiki/display/WD/Shared+OpenStack+Provider+Management#SharedOpenStackProviderManagement-VPCPackagetemplatemanagement
    inlines = [PackageComponentInline]
    package_dependant_fields = ('name', 'category', 'service_settings')
    fields = package_dependant_fields + ('archived', 'icon_url', 'description', 'product_code', 'article_code', 'unit')
    list_display = ('name', 'uuid', 'service_settings', 'price', 'archived', 'monthly_price', 'category')

    clast ServiceSettingsSharedFilter(admin.RelatedFieldListFilter):
        def field_choices(self, field, request, model_admin):
            return field.get_choices(include_blank=False, limit_choices_to={'shared': True})

    list_filter = (('service_settings', ServiceSettingsSharedFilter),)
    search_fields = ('name', 'uuid')

    def get_readonly_fields(self, request, obj=None):
        if obj and obj.is_read_only():
            return self.package_dependant_fields
        else:
            return super(PackageTemplateAdmin, self).get_readonly_fields(request, obj)

    def get_form(self, request, obj=None, **kwargs):
        form = super(PackageTemplateAdmin, self).get_form(request, obj, **kwargs)

        if 'service_settings' in form.base_fields:
            form.base_fields['service_settings'].queryset = structure_models.ServiceSettings.objects.filter(shared=True)
        return form

    def save_related(self, request, form, formsets, change):
        super(PackageTemplateAdmin, self).save_related(request, form, formsets, change)
        utils.sync_price_list_item(form.instance)


clast OpenStackPackageAdmin(admin.ModelAdmin):
    def get_project(self, obj):
        return obj.tenant.service_project_link.project.name

    get_project.short_description = _('Project')
    get_project.admin_order_field = 'tenant__service_project_link__project__name'

    def get_customer(self, obj):
        return obj.tenant.service_project_link.project.customer

    get_customer.short_description = _('Organization')
    get_customer.admin_order_field = 'tenant__service_project_link__project__customer__name'

    list_display = ('template', 'tenant', 'service_settings', 'get_project', 'get_customer')
    list_filter = ('template',)


admin.site.register(models.PackageTemplate, PackageTemplateAdmin)
admin.site.register(models.OpenStackPackage, OpenStackPackageAdmin)