Migration works correctly, DB should be prepared.
This reverts commit 466f235371.
Change-Id: Ida4e40c4cb2e7f954861465c100b46cf8196c2e5
397 lines
13 KiB
Python
397 lines
13 KiB
Python
# Copyright 2013 - 2016 Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import abc
|
|
from datetime import datetime
|
|
# pylint: disable=redefined-builtin
|
|
from functools import reduce
|
|
# pylint: enable=redefined-builtin
|
|
import operator
|
|
|
|
from django.db import models
|
|
from django.db.models.base import ModelBase
|
|
from django.db.models import query
|
|
import jsonfield
|
|
import six
|
|
|
|
from devops.error import DevopsError
|
|
from devops.helpers.helpers import deepgetattr
|
|
from devops.helpers import loader
|
|
|
|
|
|
def choices(*args, **kwargs):
|
|
defaults = {'max_length': 255, 'null': False}
|
|
defaults.update(kwargs)
|
|
defaults.update(choices=list(zip(args, args)))
|
|
return models.CharField(**defaults)
|
|
|
|
|
|
class BaseModel(models.Model):
|
|
class Meta(object):
|
|
abstract = True
|
|
|
|
created = models.DateTimeField(default=datetime.utcnow)
|
|
|
|
|
|
class ParamedModelType(ModelBase):
|
|
"""Metaclass of parameterizable class.
|
|
|
|
It implements the following functinality:
|
|
* Automaticlly sets Meta.abstract = True for all derived classes
|
|
except the first one.
|
|
* Initializes :class:`ParamFieldBase` classes with `param_key`.
|
|
* Saves the keys of :class:`ParamFieldBase` attributes in
|
|
`_param_field_names` list.
|
|
* Gives an ability to set :class:`ParamFieldBase` values in
|
|
constructor and combine the with other attributes defined in djano
|
|
model.
|
|
* Automaticlly replaces `instance.__class__` fter creation of
|
|
instance in method `__call__`. It is the major thing to make the
|
|
derived models polymorphic.
|
|
"""
|
|
|
|
# pylint: disable=bad-mcs-classmethod-argument
|
|
# noinspection PyMethodParameters
|
|
def __new__(cls, name, bases, attrs):
|
|
super_new = super(ParamedModelType, cls).__new__
|
|
|
|
# if not ParamModel itself
|
|
if name != 'ParamedModel' and name != 'NewBase':
|
|
# pylint: disable=map-builtin-not-iterating
|
|
parents = reduce(operator.add, map(lambda a: a.__mro__, bases))
|
|
# pylint: enable=map-builtin-not-iterating
|
|
# if not a first subclass of ParamedModel
|
|
if ParamedModel not in bases and ParamedModel in parents:
|
|
# add proxy=True by default
|
|
if 'Meta' not in attrs:
|
|
attrs['Meta'] = type('Meta', (object, ), {})
|
|
Meta = attrs['Meta']
|
|
Meta.proxy = True
|
|
|
|
# do django stuff
|
|
new_class = super_new(cls, name, bases, attrs)
|
|
|
|
new_class._param_field_names = []
|
|
|
|
# initialize ParamField keys
|
|
for attr_name in attrs:
|
|
attr = attrs[attr_name]
|
|
if isinstance(attr, ParamFieldBase):
|
|
attr.set_param_key(attr_name)
|
|
new_class._param_field_names.append(attr_name)
|
|
|
|
return new_class
|
|
|
|
# pylint: enable=bad-mcs-classmethod-argument
|
|
|
|
def __call__(cls, *args, **kwargs):
|
|
# split kwargs which django db are not aware of
|
|
# to separete dict
|
|
kwargs_for_params = {}
|
|
defined_params = cls.get_defined_params()
|
|
for param in defined_params:
|
|
if param in kwargs:
|
|
kwargs_for_params[param] = kwargs.pop(param)
|
|
|
|
obj = super(ParamedModelType, cls).__call__(*args, **kwargs)
|
|
|
|
if obj._class:
|
|
# we store actual class name in _class attribute
|
|
# so use it to load required class
|
|
Cls = loader.load_class(obj._class)
|
|
# replace base class
|
|
obj.__class__ = Cls
|
|
|
|
# set param values
|
|
for param in kwargs_for_params:
|
|
setattr(obj, param, kwargs_for_params[param])
|
|
|
|
return obj
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class ParamFieldBase(object):
|
|
"""Base class for ParamFields."""
|
|
|
|
def __init__(self):
|
|
self.param_key = None
|
|
|
|
def set_param_key(self, param_key):
|
|
self.param_key = param_key
|
|
|
|
@abc.abstractmethod
|
|
def set_default_value(self, instance):
|
|
return
|
|
|
|
@abc.abstractmethod
|
|
def __get__(self, instance, cls):
|
|
return
|
|
|
|
@abc.abstractmethod
|
|
def __set__(self, instance, values):
|
|
return
|
|
|
|
def __delete__(self, instance):
|
|
raise AttributeError("Can't delete attribute")
|
|
|
|
|
|
class ParamField(ParamFieldBase):
|
|
"""Field class.
|
|
|
|
This class implemets routine of using json field as a storage.
|
|
e.g. if you define a field with name "foo" then its value will be
|
|
stored in `params` json field as {'foo': 'value'}. This class
|
|
allows to avoid direct access to json field and it hides the routine.
|
|
|
|
Additionally it gives an ability:
|
|
* to set default value.
|
|
* to limit values using a list of allowed values.
|
|
|
|
Examples of usage::
|
|
|
|
class A(ParamedModel):
|
|
foo = ParamField(default=10)
|
|
bar = ParamField(choices=('a', 'b', 'c'))
|
|
|
|
a = A()
|
|
print(a.foo) # prints 10
|
|
print(a.bar) # prints None
|
|
|
|
a.foo = 5
|
|
a.bar = 'c'
|
|
print(a.params) # prints {'foo': 5, 'bar': 'c'}
|
|
|
|
a.bar = 15 # throws DevopsError
|
|
"""
|
|
|
|
def __init__(self, default=None, choices=None):
|
|
super(ParamField, self).__init__()
|
|
|
|
if choices and default not in choices:
|
|
raise DevopsError('Default value not in choices list')
|
|
|
|
self.default_value = default
|
|
self.choices = choices
|
|
|
|
def set_default_value(self, instance):
|
|
instance.params.setdefault(self.param_key, self.default_value)
|
|
|
|
def __get__(self, instance, cls):
|
|
return instance.params.get(self.param_key, self.default_value)
|
|
|
|
def __set__(self, instance, value):
|
|
if self.choices and value not in self.choices:
|
|
raise DevopsError('{}: Value not in choices list'
|
|
''.format(self.param_key))
|
|
|
|
instance.params[self.param_key] = value
|
|
|
|
|
|
class ParamMultiField(ParamFieldBase):
|
|
"""Field class which stores other fields.
|
|
|
|
Acts the same way as :class:`ParamField` but should be used in case
|
|
if you want to use nested fields.
|
|
|
|
Examples of ussage::
|
|
|
|
class A(ParamedModel):
|
|
foo = ParamMultiField(
|
|
bar=ParamField(default=10),
|
|
baz=ParamField(),
|
|
)
|
|
|
|
a = A()
|
|
print(a.foo.bar) # prints 10
|
|
print(a.foo.baz) # prints None
|
|
|
|
a.foo.bar = 0
|
|
a.foo.baz = 1
|
|
print(a.params) # prints {'foo': {'bar': 0, 'baz': 1}}
|
|
"""
|
|
|
|
def __init__(self, **subfields):
|
|
super(ParamMultiField, self).__init__()
|
|
|
|
if len(subfields) == 0:
|
|
raise DevopsError('subfields is empty')
|
|
|
|
self.subfields = []
|
|
for name, field in subfields.items():
|
|
if not isinstance(field, ParamFieldBase):
|
|
raise DevopsError('field "{}" has wrong type;'
|
|
' should be ParamFieldBase subclass instance'
|
|
''.format(name))
|
|
field.set_param_key(name)
|
|
self.subfields.append(field)
|
|
|
|
self.choices = None
|
|
self._proxy = None
|
|
|
|
self.proxy_fields = {field.param_key: field
|
|
for field in self.subfields}
|
|
Proxy = type('ParamMultiFieldProxy', (object, ), self.proxy_fields)
|
|
self._proxy = Proxy()
|
|
|
|
def set_default_value(self, instance):
|
|
for field in self.subfields:
|
|
self._init_proxy_params(instance)
|
|
field.set_default_value(self._proxy)
|
|
|
|
def _init_proxy_params(self, instance):
|
|
instance.params.setdefault(self.param_key, dict())
|
|
self._proxy.params = instance.params[self.param_key]
|
|
|
|
def __get__(self, instance, cls):
|
|
self._init_proxy_params(instance)
|
|
return self._proxy
|
|
|
|
def __set__(self, instance, values):
|
|
if not isinstance(values, dict):
|
|
raise DevopsError('Can set only dict')
|
|
self._init_proxy_params(instance)
|
|
for field_name, field_value in values.items():
|
|
if field_name not in self.proxy_fields:
|
|
raise DevopsError('Unknown field "{}"'.format(field_name))
|
|
setattr(self._proxy, field_name, field_value)
|
|
|
|
|
|
class ParamedModelQuerySet(query.QuerySet):
|
|
"""Custom QuerySet for ParamedModel"""
|
|
|
|
def __get_all_field_names(self):
|
|
field_names = set()
|
|
_meta = self.model._meta
|
|
fields = _meta.get_fields()
|
|
for field in fields:
|
|
# For backwards compatibility GenericForeignKey should not be
|
|
# included in the results.
|
|
if field.is_relation and field.many_to_one and \
|
|
field.related_model is None:
|
|
continue
|
|
# Relations to child proxy models should not be included.
|
|
if field.model != _meta.model and\
|
|
field.model._meta.concrete_model == _meta.concrete_model:
|
|
continue
|
|
|
|
field_names.add(field.name)
|
|
if hasattr(field, 'attname'):
|
|
field_names.add(field.attname)
|
|
return field_names
|
|
|
|
def filter(self, *args, **kwargs):
|
|
super_filter = super(ParamedModelQuerySet, self).filter
|
|
|
|
# split kwargs which django db are not aware of
|
|
# to separate dict
|
|
kwargs_for_params = {}
|
|
db_kwargs = {}
|
|
|
|
# Fix deprecated code usage from django
|
|
field_names = self.__get_all_field_names()
|
|
|
|
for param in kwargs.keys():
|
|
first_subparam = param.split('__')[0]
|
|
if first_subparam not in field_names:
|
|
kwargs_for_params[param] = kwargs[param]
|
|
else:
|
|
db_kwargs[param] = kwargs[param]
|
|
|
|
# filter using db arguments
|
|
queryset = super_filter(*args, **db_kwargs)
|
|
|
|
if not kwargs_for_params:
|
|
# return db queryset if there is no params
|
|
return queryset
|
|
|
|
# filter using params
|
|
result_ids = []
|
|
for item in queryset:
|
|
for key, value in kwargs_for_params.items():
|
|
# NOTE(astudenov): no support for 'gt', 'lt', 'in'
|
|
# and other django's filter stuff
|
|
|
|
if not isinstance(item, self.model):
|
|
# skip other classes
|
|
continue
|
|
|
|
item_val = deepgetattr(item, key, splitter='__',
|
|
do_raise=True)
|
|
if item_val != value:
|
|
break
|
|
else:
|
|
result_ids.append(item.id)
|
|
|
|
# convert result to new queryset using ids
|
|
return super_filter(id__in=result_ids)
|
|
|
|
|
|
class ParamedModelManager(models.Manager):
|
|
"""Manager for ParamedModel"""
|
|
|
|
use_for_related_fields = True
|
|
|
|
def get_queryset(self):
|
|
return ParamedModelQuerySet(self.model, using=self._db)
|
|
|
|
|
|
class ParamedModel(six.with_metaclass(ParamedModelType, models.Model)):
|
|
"""Parameterizable class
|
|
|
|
This class allows all derived classes to be polymorphically extended
|
|
by extra fields by using :class:`ParamField` and :class:`ParamMultiField`.
|
|
First derived class of :class:`ParamModel` must be non-abstract to
|
|
create a real database table and all subsequent derived classes are
|
|
atomaticlly marked as abstract by :class:`ParamedModelType`. This is
|
|
made to avoid creation of db tables for derived classes. See mode
|
|
info about how it is implemented in metaclass :class:`ParamedModelType`.
|
|
|
|
The class has two fields:
|
|
* params - this is a jsonfield where all extra fields are stored and
|
|
serialized to json string when the instance is saved to db
|
|
* _class - a :class:`ParamField` which stores path to the derived
|
|
class. It allows to load the same derived class after
|
|
loading data from db.
|
|
"""
|
|
|
|
class Meta(object):
|
|
abstract = True
|
|
|
|
objects = ParamedModelManager()
|
|
|
|
params = jsonfield.JSONField(default={})
|
|
_class = ParamField()
|
|
|
|
@classmethod
|
|
def get_defined_params(cls):
|
|
param_names = []
|
|
for basecls in cls.__mro__:
|
|
if not hasattr(basecls, '_param_field_names'):
|
|
continue
|
|
param_names += basecls._param_field_names
|
|
return param_names
|
|
|
|
def set_default_params(self):
|
|
for basecls in self.__class__.__mro__:
|
|
if not hasattr(basecls, '_param_field_names'):
|
|
continue
|
|
for param in basecls._param_field_names:
|
|
basecls.__dict__[param].set_default_value(self)
|
|
|
|
def save(self, *args, **kwargs):
|
|
# store current class to _class attribute
|
|
self._class = loader.get_class_path(self)
|
|
self.set_default_params()
|
|
return super(ParamedModel, self).save(*args, **kwargs)
|